Pathauto and dependencies

This commit is contained in:
Rob Davies 2017-05-22 15:12:47 +01:00
parent 4b1a293d57
commit 24ffcb956b
257 changed files with 29510 additions and 0 deletions

View file

@ -0,0 +1,9 @@
<?php
namespace Drupal\ctools\Access;
use Drupal\Core\Session\AccountInterface;
interface AccessInterface {
public function access(AccountInterface $account);
}

View file

@ -0,0 +1,51 @@
<?php
namespace Drupal\ctools\Access;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Routing\Access\AccessInterface as CoreAccessInterface;
use Drupal\Core\Routing\RouteMatch;
use Drupal\Core\Session\AccountInterface;
use Drupal\ctools\Access\AccessInterface as CToolsAccessInterface;
use Drupal\user\SharedTempStoreFactory;
use Symfony\Component\Routing\Route;
class TempstoreAccess implements CoreAccessInterface {
/**
* The shared tempstore factory.
*
* @var \Drupal\user\SharedTempStoreFactory
*/
protected $tempstore;
public function __construct(SharedTempStoreFactory $tempstore) {
$this->tempstore = $tempstore;
}
protected function getTempstore() {
return $this->tempstore;
}
public function access(Route $route, RouteMatch $match, AccountInterface $account) {
$tempstore_id = $match->getParameter('tempstore_id') ? $match->getParameter('tempstore_id') : $route->getDefault('tempstore_id');
$id = $match->getParameter($route->getRequirement('_ctools_access'));
if ($tempstore_id && $id) {
$cached_values = $this->getTempstore()->get($tempstore_id)->get($id);
if (!empty($cached_values['access']) && ($cached_values['access'] instanceof CToolsAccessInterface)) {
$access = $cached_values['access']->access($account);
}
else {
$access = AccessResult::allowed();
}
}
else {
$access = AccessResult::forbidden();
}
// The different wizards will have different tempstore ids and adding this
// cache context allows us to nuance the access per wizard.
$access->addCacheContexts(['url.query_args:tempstore_id']);
return $access;
}
}

View file

@ -0,0 +1,23 @@
<?php
namespace Drupal\ctools\Ajax;
use Drupal\Core\Ajax\OpenModalDialogCommand;
class OpenModalWizardCommand extends OpenModalDialogCommand {
public function __construct($object, $tempstore_id, array $parameters = array(), array $dialog_options = array(), $settings = NULL) {
// Instantiate the wizard class properly.
$parameters += [
'tempstore_id' => $tempstore_id,
'machine_name' => NULL,
'step' => NULL,
];
$form = \Drupal::service('ctools.wizard.factory')->getWizardForm($object, $parameters, TRUE);
$title = isset($form['#title']) ? $form['#title'] : '';
$content = $form;
parent::__construct($title, $content, $dialog_options, $settings);
}
}

View file

@ -0,0 +1,54 @@
<?php
namespace Drupal\ctools\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines a Relationship item annotation object.
*
* @see \Drupal\ctools\Plugin\RelationshipManager
* @see plugin_api
*
* @Annotation
*/
class Relationship extends Plugin {
/**
* The plugin ID.
*
* @var string
*/
public $id;
/**
* The label of the plugin.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $label;
/**
* The returned data type of this relationship
*
* @var string
*/
public $data_type;
/**
* The name of the property from which this relationship is derived.
*
* @var string
*/
public $property_name;
/**
* The array of contexts requires or optional for this plugin.
*
* @var \Drupal\Core\Plugin\Context\ContextInterface[]
*/
public $context;
}

View file

@ -0,0 +1,26 @@
<?php
namespace Drupal\ctools;
interface ConstraintConditionInterface {
/**
* Applies relevant constraints for this condition to the injected contexts.
*
* @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts
*
* @return NULL
*/
public function applyConstraints(array $contexts = array());
/**
* Removes constraints for this condition from the injected contexts.
*
* @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts
*
* @return NULL
*/
public function removeConstraints(array $contexts = array());
}

View file

@ -0,0 +1,25 @@
<?php
namespace Drupal\ctools\Context;
use Drupal\Core\Plugin\Context\Context;
/**
* Provides a class to indicate that this context is always present.
*
* @internal
*
* @todo Move into core.
*/
class AutomaticContext extends Context {
/**
* Returns TRUE if this context is automatic and always available.
*
* @return bool
*/
public function isAutomatic() {
return TRUE;
}
}

View file

@ -0,0 +1,66 @@
<?php
namespace Drupal\ctools\Context;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Plugin\Context\Context;
use Drupal\Core\Plugin\Context\ContextDefinitionInterface;
/**
* @todo.
*/
class EntityLazyLoadContext extends Context {
/**
* The entity UUID.
*
* @var string
*/
protected $uuid;
/**
* The entity repository.
*
* @var \Drupal\Core\Entity\EntityRepositoryInterface
*/
protected $entityRepository;
/**
* Construct an EntityLazyLoadContext object.
*
* @param \Drupal\Core\Plugin\Context\ContextDefinitionInterface $context_definition
* The context definition.
* @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
* The entity repository.
* @param string $uuid
* The UUID of the entity.
*/
public function __construct(ContextDefinitionInterface $context_definition, EntityRepositoryInterface $entity_repository, $uuid) {
parent::__construct($context_definition);
$this->entityRepository = $entity_repository;
$this->uuid = $uuid;
}
/**
* {@inheritdoc}
*/
public function getContextValue() {
if (!$this->contextData) {
$entity_type_id = substr($this->contextDefinition->getDataType(), 7);
$this->setContextValue($this->entityRepository->loadEntityByUuid($entity_type_id, $this->uuid));
}
return parent::getContextValue();
}
/**
* {@inheritdoc}
*/
public function hasContextValue() {
// Ensure that the entity is loaded before checking if it exists.
if (!$this->contextData) {
$this->getContextValue();
}
return parent::hasContextValue();
}
}

View file

@ -0,0 +1,50 @@
<?php
namespace Drupal\ctools;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Plugin\Context\Context;
use Drupal\Core\Plugin\Context\ContextDefinition;
use Drupal\ctools\Context\EntityLazyLoadContext;
/**
* Maps context configurations to context objects.
*/
class ContextMapper implements ContextMapperInterface {
/**
* The entity repository.
*
* @var \Drupal\Core\Entity\EntityRepositoryInterface
*/
protected $entityRepository;
/**
* Constructs a new ContextMapper.
*
* @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
* The entity repository.
*/
public function __construct(EntityRepositoryInterface $entity_repository) {
$this->entityRepository = $entity_repository;
}
/**
* {@inheritdoc}
*/
public function getContextValues(array $context_configurations) {
$contexts = [];
foreach ($context_configurations as $name => $context_configuration) {
$context_definition = new ContextDefinition($context_configuration['type'], $context_configuration['label'], TRUE, FALSE, $context_configuration['description']);
if (strpos($context_configuration['type'], 'entity:') === 0) {
$context = new EntityLazyLoadContext($context_definition, $this->entityRepository, $context_configuration['value']);
}
else {
$context = new Context($context_definition, $context_configuration['value']);
}
$contexts[$name] = $context;
}
return $contexts;
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace Drupal\ctools;
/**
* Provides an interface for mapping context configurations to context objects.
*/
interface ContextMapperInterface {
/**
* Gathers the static context values.
*
* @param array[] $static_context_configurations
* An array of static context configurations.
*
* @return \Drupal\Component\Plugin\Context\ContextInterface[]
* An array of set context values, keyed by context name.
*/
public function getContextValues(array $static_context_configurations);
}

View file

@ -0,0 +1,6 @@
<?php
namespace Drupal\ctools;
class ContextNotFoundException extends \Exception {}

View file

@ -0,0 +1,52 @@
<?php
namespace Drupal\ctools\Controller;
use Drupal\Core\Controller\ControllerResolverInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\ctools\Wizard\WizardFactoryInterface;
/**
* Wrapping controller for wizard forms that serve as the main page body.
*/
class WizardEntityFormController extends WizardFormController {
/**
* The entity manager service.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver
* The controller resolver.
* @param \Drupal\Core\Form\FormBuilderInterface $form_builder
* The form builder.
* @param \Drupal\ctools\Wizard\WizardFactoryInterface $wizard_factory
* The wizard factory.
* @param \Drupal\Core\Entity\EntityManagerInterface $manager
* The entity manager.
*/
public function __construct(ControllerResolverInterface $controller_resolver, FormBuilderInterface $form_builder, WizardFactoryInterface $wizard_factory, EntityManagerInterface $manager) {
parent::__construct($controller_resolver, $form_builder, $wizard_factory);
$this->entityManager = $manager;
}
/**
* {@inheritdoc}
*/
protected function getFormArgument(RouteMatchInterface $route_match) {
$form_arg = $route_match->getRouteObject()->getDefault('_entity_wizard');
list($entity_type_id, $operation) = explode('.', $form_arg);
$definition = $this->entityManager->getDefinition($entity_type_id);
$handlers = $definition->getHandlerClasses();
if (empty($handlers['wizard'][$operation])) {
throw new \Exception(sprintf('Unsupported wizard operation %s', $operation));
}
return $handlers['wizard'][$operation];
}
}

View file

@ -0,0 +1,82 @@
<?php
namespace Drupal\ctools\Controller;
use Drupal\Core\Controller\ControllerResolverInterface;
use Drupal\Core\Controller\FormController;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\ctools\Wizard\FormWizardInterface;
use Drupal\ctools\Wizard\WizardFactoryInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Wrapping controller for wizard forms that serve as the main page body.
*/
class WizardFormController extends FormController {
/**
* The class resolver.
*
* @var \Drupal\Core\DependencyInjection\ClassResolverInterface;
*/
protected $classResolver;
/**
* Tempstore Factory for keeping track of values in each step of the wizard.
*
* @var \Drupal\user\SharedTempStoreFactory
*/
protected $tempstore;
/**
* The event dispatcher.
*
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
protected $dispatcher;
/**
* @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver
* The controller resolver.
* @param \Drupal\Core\Form\FormBuilderInterface $form_builder
* The form builder.
* @param \Drupal\ctools\Wizard\WizardFactoryInterface $wizard_factory
* The wizard factory.
*/
public function __construct(ControllerResolverInterface $controller_resolver, FormBuilderInterface $form_builder, WizardFactoryInterface $wizard_factory) {
parent::__construct($controller_resolver, $form_builder);
$this->wizardFactory = $wizard_factory;
}
/**
* {@inheritdoc}
*/
protected function getFormArgument(RouteMatchInterface $route_match) {
return $route_match->getRouteObject()->getDefault('_wizard');
}
/**
* Wizards are not instantiated as simply as forms, so this method is unused.
*/
protected function getFormObject(RouteMatchInterface $route_match, $form_arg) {
if (!is_subclass_of($form_arg, '\Drupal\ctools\Wizard\FormWizardInterface')) {
throw new \Exception("The _wizard default must reference a class instance of \\Drupal\\ctools\\Wizard\\FormWizardInterface.");
}
$parameters = $route_match->getParameters()->all();
$parameters += $form_arg::getParameters();
$parameters['route_match'] = $route_match;
return $this->wizardFactory->createWizard($form_arg, $parameters);
}
/**
* {@inheritdoc}
*/
public function getContentResult(Request $request, RouteMatchInterface $route_match) {
$wizard = $this->getFormObject($route_match, $this->getFormArgument($route_match));
$ajax = $request->attributes->get('js') == 'ajax' ? TRUE : FALSE;
return $this->wizardFactory->getWizardForm($wizard, $request->attributes->all(), $ajax);
}
}

View file

@ -0,0 +1,41 @@
<?php
namespace Drupal\ctools\Event;
use Drupal\ctools\Wizard\FormWizardInterface;
use Symfony\Component\EventDispatcher\Event;
/**
* An event for altering form wizard values.
*/
class WizardEvent extends Event {
/**
* @var \Drupal\ctools\Wizard\FormWizardInterface
*/
protected $wizard;
/**
* @var mixed
*/
protected $values;
function __construct(FormWizardInterface $wizard, $values) {
$this->wizard = $wizard;
$this->values = $values;
}
public function getWizard() {
return $this->wizard;
}
public function getValues() {
return $this->values;
}
public function setValues($values) {
$this->values = $values;
return $this;
}
}

View file

@ -0,0 +1,43 @@
<?php
namespace Drupal\ctools\Form;
use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\NestedArray;
/**
* Provides helper methods for using an AJAX modal.
*/
trait AjaxFormTrait {
/**
* Gets attributes for use with an AJAX modal.
*
* @return array
*/
public static function getAjaxAttributes() {
return [
'class' => ['use-ajax'],
'data-dialog-type' => 'modal',
'data-dialog-options' => Json::encode([
'width' => 'auto',
]),
];
}
/**
* Gets attributes for use with an add button AJAX modal.
*
* @return array
*/
public static function getAjaxButtonAttributes() {
return NestedArray::mergeDeep(AjaxFormTrait::getAjaxAttributes(), [
'class' => [
'button',
'button--small',
'button-action',
],
]);
}
}

View file

@ -0,0 +1,182 @@
<?php
namespace Drupal\ctools\Form;
use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\Component\Uuid\Uuid;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\CloseModalDialogCommand;
use Drupal\Core\Ajax\RedirectCommand;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContextAwarePluginInterface;
use Drupal\ctools\ConstraintConditionInterface;
use Drupal\user\SharedTempStoreFactory;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Base class for condition configur operations.
*/
abstract class ConditionConfigure extends FormBase {
/**
* @var \Drupal\user\SharedTempStoreFactory
*/
protected $tempstore;
/**
* @var \Drupal\Core\Condition\ConditionManager
*/
protected $manager;
/**
* @var string
*/
protected $tempstore_id;
/**
* @var string;
*/
protected $machine_name;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static($container->get('user.shared_tempstore'), $container->get('plugin.manager.condition'));
}
function __construct(SharedTempStoreFactory $tempstore, PluginManagerInterface $manager) {
$this->tempstore = $tempstore;
$this->manager = $manager;
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'ctools_condition_configure';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, $condition = NULL, $tempstore_id = NULL, $machine_name = NULL) {
$this->tempstore_id = $tempstore_id;
$this->machine_name = $machine_name;
$cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name);
if (is_numeric($condition) || Uuid::isValid($condition)) {
$id = $condition;
$condition = $this->getConditions($cached_values)[$id];
$instance = $this->manager->createInstance($condition['id'], $condition);
}
else {
$instance = $this->manager->createInstance($condition, []);
}
$form_state->setTemporaryValue('gathered_contexts', $this->getContexts($cached_values));
/** @var $instance \Drupal\Core\Condition\ConditionInterface */
$form = $instance->buildConfigurationForm($form, $form_state);
if (isset($id)) {
// Conditionally set this form element so that we can update or add.
$form['id'] = [
'#type' => 'value',
'#value' => $id
];
}
$form['instance'] = [
'#type' => 'value',
'#value' => $instance
];
$form['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Save'),
'#ajax' => [
'callback' => [$this, 'ajaxSave'],
]
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name);
/** @var $instance \Drupal\Core\Condition\ConditionInterface */
$instance = $form_state->getValue('instance');
$instance->submitConfigurationForm($form, $form_state);
$conditions = $this->getConditions($cached_values);
if ($instance instanceof ContextAwarePluginInterface) {
/** @var $instance \Drupal\Core\Plugin\ContextAwarePluginInterface */
$context_mapping = $form_state->hasValue('context_mapping')? $form_state->getValue('context_mapping') : [];
$instance->setContextMapping($context_mapping);
}
if ($instance instanceof ConstraintConditionInterface) {
/** @var $instance \Drupal\ctools\ConstraintConditionInterface */
$instance->applyConstraints($this->getContexts($cached_values));
}
if ($form_state->hasValue('id')) {
$conditions[$form_state->getValue('id')] = $instance->getConfiguration();
}
else {
$conditions[] = $instance->getConfiguration();
}
$cached_values = $this->setConditions($cached_values, $conditions);
$this->tempstore->get($this->tempstore_id)->set($this->machine_name, $cached_values);
list($route_name, $route_parameters) = $this->getParentRouteInfo($cached_values);
$form_state->setRedirect($route_name, $route_parameters);
}
public function ajaxSave(array &$form, FormStateInterface $form_state) {
$response = new AjaxResponse();
$cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name);
list($route_name, $route_parameters) = $this->getParentRouteInfo($cached_values);
$response->addCommand(new RedirectCommand($this->url($route_name, $route_parameters)));
$response->addCommand(new CloseModalDialogCommand());
return $response;
}
/**
* Document the route name and parameters for redirect after submission.
*
* @param $cached_values
*
* @return array
* In the format of
* return ['route.name', ['machine_name' => $this->machine_name, 'step' => 'step_name']];
*/
abstract protected function getParentRouteInfo($cached_values);
/**
* Custom logic for retrieving the conditions array from cached_values.
*
* @param $cached_values
*
* @return array
*/
abstract protected function getConditions($cached_values);
/**
* Custom logic for setting the conditions array in cached_values.
*
* @param $cached_values
*
* @param $conditions
* The conditions to set within the cached values.
*
* @return mixed
* Return the $cached_values
*/
abstract protected function setConditions($cached_values, $conditions);
/**
* Custom logic for retrieving the contexts array from cached_values.
*
* @param $cached_values
*
* @return \Drupal\Core\Plugin\Context\ContextInterface[]
*/
abstract protected function getContexts($cached_values);
}

View file

@ -0,0 +1,210 @@
<?php
namespace Drupal\ctools\Form;
use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\ConfirmFormHelper;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Drupal\ctools\ConstraintConditionInterface;
use Drupal\user\SharedTempStoreFactory;
use Symfony\Component\DependencyInjection\ContainerInterface;
abstract class ConditionDelete extends ConfirmFormBase {
/**
* @var \Drupal\user\SharedTempStoreFactory
*/
protected $tempstore;
/**
* @var \Drupal\Core\Condition\ConditionManager
*/
protected $manager;
/**
* @var string
*/
protected $tempstore_id;
/**
* @var string;
*/
protected $machine_name;
/**
* @var int;
*/
protected $id;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static($container->get('user.shared_tempstore'), $container->get('plugin.manager.condition'));
}
function __construct(SharedTempStoreFactory $tempstore, PluginManagerInterface $manager) {
$this->tempstore = $tempstore;
$this->manager = $manager;
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'ctools_condition_delete';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, $id = NULL, $tempstore_id = NULL, $machine_name = NULL) {
$this->tempstore_id = $tempstore_id;
$this->machine_name = $machine_name;
$this->id = $id;
$cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name);
$form ['#title'] = $this->getQuestion($id, $cached_values);
$form ['#attributes']['class'][] = 'confirmation';
$form ['description'] = array('#markup' => $this->getDescription());
$form [$this->getFormName()] = array('#type' => 'hidden', '#value' => 1);
// By default, render the form using theme_confirm_form().
if (!isset($form ['#theme'])) {
$form ['#theme'] = 'confirm_form';
}
$form['actions'] = array('#type' => 'actions');
$form['actions'] += $this->actions($form, $form_state);
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name);
$conditions = $this->getConditions($cached_values);
/** @var $instance \Drupal\ctools\ConstraintConditionInterface */
$instance = $this->manager->createInstance($conditions[$this->id]['id'], $conditions[$this->id]);
if ($instance instanceof ConstraintConditionInterface) {
$instance->removeConstraints($this->getContexts($cached_values));
}
unset($conditions[$this->id]);
$cached_values = $this->setConditions($cached_values, $conditions);
$this->tempstore->get($this->tempstore_id)->set($this->machine_name, $cached_values);
list($route_name, $route_parameters) = $this->getParentRouteInfo($cached_values);
$form_state->setRedirect($route_name, $route_parameters);
}
public function getQuestion($id = NULL, $cached_values = NULL) {
$condition = $this->getConditions($cached_values)[$id];
return $this->t('Are you sure you want to delete the @label condition?', array(
'@label' => $condition['id'],
));
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return $this->t('This action cannot be undone.');
}
/**
* {@inheritdoc}
*/
public function getFormName() {
return 'confirm';
}
/**
* {@inheritdoc}
*/
protected function actions(array $form, FormStateInterface $form_state) {
return array(
'submit' => array(
'#type' => 'submit',
'#value' => $this->getConfirmText(),
'#validate' => array(
array($this, 'validateForm'),
),
'#submit' => array(
array($this, 'submitForm'),
),
),
'cancel' => ConfirmFormHelper::buildCancelLink($this, $this->getRequest()),
);
}
/**
* Returns the route to go to if the user cancels the action.
*
* @return \Drupal\Core\Url
* A URL object.
*/
public function getCancelUrl() {
$cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name);
list($route_name, $route_parameters) = $this->getParentRouteInfo($cached_values);
return new Url($route_name, $route_parameters);
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Delete');
}
/**
* {@inheritdoc}
*/
public function getCancelText() {
return $this->t('Cancel');
}
/**
* Document the route name and parameters for redirect after submission.
*
* @param $cached_values
*
* @return array
* In the format of
* return ['route.name', ['machine_name' => $this->machine_name, 'step' => 'step_name]];
*/
abstract protected function getParentRouteInfo($cached_values);
/**
* Custom logic for retrieving the conditions array from cached_values.
*
* @param $cached_values
*
* @return array
*/
abstract protected function getConditions($cached_values);
/**
* Custom logic for setting the conditions array in cached_values.
*
* @param $cached_values
*
* @param $conditions
* The conditions to set within the cached values.
*
* @return mixed
* Return the $cached_values
*/
abstract protected function setConditions($cached_values, $conditions);
/**
* Custom logic for retrieving the contexts array from cached_values.
*
* @param $cached_values
*
* @return \Drupal\Core\Plugin\Context\ContextInterface[]
*/
abstract protected function getContexts($cached_values);
}

View file

@ -0,0 +1,254 @@
<?php
namespace Drupal\ctools\Form;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\CloseModalDialogCommand;
use Drupal\Core\Ajax\RedirectCommand;
use Drupal\Core\Entity\Entity;
use Drupal\Core\Entity\Plugin\DataType\EntityAdapter;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\Context\Context;
use Drupal\Core\Plugin\Context\ContextDefinition;
use Drupal\Core\Plugin\Context\ContextInterface;
use Drupal\Core\Url;
use Drupal\user\SharedTempStoreFactory;
use Symfony\Component\DependencyInjection\ContainerInterface;
abstract class ContextConfigure extends FormBase {
/**
* @var \Drupal\user\SharedTempStoreFactory
*/
protected $tempstore;
/**
* @var string
*/
protected $tempstore_id;
/**
* @var string;
*/
protected $machine_name;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static($container->get('user.shared_tempstore'));
}
function __construct(SharedTempStoreFactory $tempstore) {
$this->tempstore = $tempstore;
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'ctools_context_configure';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, $context_id = NULL, $tempstore_id = NULL, $machine_name = NULL) {
$this->tempstore_id = $tempstore_id;
$this->machine_name = $machine_name;
$cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name);
$contexts = $this->getContexts($cached_values);
$edit = FALSE;
if (!empty($contexts[$context_id])) {
$context = $contexts[$context_id];
$machine_name = $context_id;
$edit = TRUE;
}
else {
$context_definition = new ContextDefinition($context_id);
$context = new Context($context_definition);
$machine_name = '';
}
$label = $context->getContextDefinition()->getLabel();
$description = $context->getContextDefinition()->getDescription();
$data_type = $context->getContextDefinition()->getDataType();
$form['#attached']['library'][] = 'core/drupal.dialog.ajax';
$form['context_id'] = [
'#type' => 'value',
'#value' => $context_id
];
$form['label'] = [
'#type' => 'textfield',
'#title' => $this->t('Label'),
'#default_value' => $label,
'#required' => TRUE,
];
$form['machine_name'] = [
'#type' => 'machine_name',
'#title' => $this->t('Machine Name'),
'#default_value' => $machine_name,
'#required' => TRUE,
'#maxlength' => 128,
'#machine_name' => [
'source' => ['label'],
'exists' => [$this, 'contextExists'],
],
'#disabled' => $this->disableMachineName($cached_values, $machine_name),
];
$form['description'] = [
'#type' => 'textarea',
'#title' => $this->t('Description'),
'#default_value' => $description,
];
if (strpos($data_type, 'entity:') === 0) {
list(, $entity_type) = explode(':', $data_type);
/** @var EntityAdapter $entity */
$entity = $edit ? $context->getContextValue() : NULL;
$form['context_value'] = [
'#type' => 'entity_autocomplete',
'#required' => TRUE,
'#target_type' => $entity_type,
'#default_value' => $entity,
'#title' => $this->t('Select entity'),
];
}
else {
$value = $context->getContextData()->getValue();
$form['context_value'] = [
'#title' => $this->t('Set a context value'),
'#type' => 'textfield',
'#required' => TRUE,
'#default_value' => $value,
];
}
$form['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Save'),
'#ajax' => [
'callback' => [$this, 'ajaxSave'],
]
];
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
// If these are not equal, then we're adding a new context and should not override an existing context.
if ($form_state->getValue('machine_name') != $form_state->getValue('context_id')) {
$machine_name = $form_state->getValue('machine_name');
$cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name);
if (!empty($this->getContexts($cached_values)[$machine_name])) {
$form_state->setError($form['machine_name'], $this->t('That machine name is in use by another context definition.'));
}
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name);
$contexts = $this->getContexts($cached_values);
if ($form_state->getValue('machine_name') != $form_state->getValue('context_id')) {
$data_type = $form_state->getValue('context_id');
$context_definition = new ContextDefinition($data_type, $form_state->getValue('label'), TRUE, FALSE, $form_state->getValue('description'));
}
else {
$context = $contexts[$form_state->getValue('machine_name')];
$context_definition = $context->getContextDefinition();
$context_definition->setLabel($form_state->getValue('label'));
$context_definition->setDescription($form_state->getValue('description'));
}
// We're dealing with an entity and should make sure it's loaded.
if (strpos($context_definition->getDataType(), 'entity:') === 0) {
list(, $entity_type) = explode(':', $context_definition->getDataType());
if (is_numeric($form_state->getValue('context_value'))) {
$value = \Drupal::entityTypeManager()->getStorage($entity_type)->load($form_state->getValue('context_value'));
}
}
// No loading required for non-entity values.
else {
$value = $form_state->getValue('context_value');
}
$context = new Context($context_definition, $value);
$cached_values = $this->addContext($cached_values, $form_state->getValue('machine_name'), $context);
$this->tempstore->get($this->tempstore_id)->set($this->machine_name, $cached_values);
list($route_name, $route_parameters) = $this->getParentRouteInfo($cached_values);
$form_state->setRedirect($route_name, $route_parameters);
}
public function ajaxSave(array &$form, FormStateInterface $form_state) {
$response = new AjaxResponse();
$cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name);
list($route_name, $route_parameters) = $this->getParentRouteInfo($cached_values);
$url = new Url($route_name, $route_parameters);
$response->addCommand(new RedirectCommand($url->toString()));
$response->addCommand(new CloseModalDialogCommand());
return $response;
}
/**
* Document the route name and parameters for redirect after submission.
*
* @param $cached_values
*
* @return array
* In the format of
* return ['route.name', ['machine_name' => $this->machine_name, 'step' => 'step_name]];
*/
abstract protected function getParentRouteInfo($cached_values);
/**
* Custom logic for retrieving the contexts array from cached_values.
*
* @param $cached_values
*
* @return \Drupal\Core\Plugin\Context\ContextInterface[]
*/
abstract protected function getContexts($cached_values);
/**
* Custom logic for adding a context to the cached_values contexts array.
*
* @param array $cached_values
* The cached_values currently in use.
* @param string $context_id
* The context identifier.
* @param \Drupal\Core\Plugin\Context\ContextInterface $context
* The context to add or update within the cached values.
*
* @return mixed
* Return the $cached_values
*/
abstract protected function addContext($cached_values, $context_id, ContextInterface $context);
/**
* Custom "exists" logic for the context to be created.
*
* @param string $value
* The name of the context.
* @param $element
* The machine_name element
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return bool
* Return true if a context of this name exists.
*/
abstract public function contextExists($value, $element, $form_state);
/**
* Determines if the machine_name should be disabled.
*
* @param $cached_values
*
* @return bool
*/
abstract protected function disableMachineName($cached_values, $machine_name);
}

View file

@ -0,0 +1,85 @@
<?php
namespace Drupal\ctools\Form;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\user\SharedTempStoreFactory;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a form for deleting an contexts and relationships.
*/
abstract class ContextDelete extends ConfirmFormBase {
/**
* @var \Drupal\user\SharedTempStoreFactory
*/
protected $tempstore;
/**
* @var string
*/
protected $tempstore_id;
/**
* @var string
*/
protected $machine_name;
/**
* The static context's machine name.
*
* @var array
*/
protected $context_id;
public static function create(ContainerInterface $container) {
return new static($container->get('user.shared_tempstore'));
}
public function __construct(SharedTempStoreFactory $tempstore) {
$this->tempstore = $tempstore;
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'ctools_context_delete_form';
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Delete');
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, $tempstore_id = NULL, $machine_name = NULL, $context_id = NULL) {
$this->tempstore_id = $tempstore_id;
$this->machine_name = $machine_name;
$this->context_id = $context_id;
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$form_state->setRedirectUrl($this->getCancelUrl());
}
protected function getTempstore() {
return $this->tempstore->get($this->tempstore_id)->get($this->machine_name);
}
protected function setTempstore($cached_values) {
$this->tempstore->get($this->tempstore_id)->set($this->machine_name, $cached_values);
}
}

View file

@ -0,0 +1,224 @@
<?php
namespace Drupal\ctools\Form;
use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\OpenModalDialogCommand;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
abstract class ManageConditions extends FormBase {
/**
* @var \Drupal\Core\Condition\ConditionManager
*/
protected $manager;
/**
* @var string
*/
protected $machine_name;
public static function create(ContainerInterface $container) {
return new static($container->get('plugin.manager.condition'));
}
function __construct(PluginManagerInterface $manager) {
$this->manager = $manager;
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'ctools_manage_conditions_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$cached_values = $form_state->getTemporaryValue('wizard');
$this->machine_name = $cached_values['id'];
$form['#attached']['library'][] = 'core/drupal.dialog.ajax';
$options = [];
$contexts = $this->getContexts($cached_values);
foreach ($this->manager->getDefinitionsForContexts($contexts) as $plugin_id => $definition) {
$options[$plugin_id] = (string) $definition['label'];
}
$form['items'] = array(
'#type' => 'markup',
'#prefix' => '<div id="configured-conditions">',
'#suffix' => '</div>',
'#theme' => 'table',
'#header' => array($this->t('Plugin Id'), $this->t('Summary'), $this->t('Operations')),
'#rows' => $this->renderRows($cached_values),
'#empty' => $this->t('No required conditions have been configured.')
);
$form['conditions'] = [
'#type' => 'select',
'#options' => $options,
];
$form['add'] = [
'#type' => 'submit',
'#name' => 'add',
'#value' => $this->t('Add Condition'),
'#ajax' => [
'callback' => [$this, 'add'],
'event' => 'click',
],
'#submit' => [
'callback' => [$this, 'submitForm'],
]
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$cached_values = $form_state->getTemporaryValue('wizard');
list(, $route_parameters) = $this->getOperationsRouteInfo($cached_values, $this->machine_name, $form_state->getValue('conditions'));
$form_state->setRedirect($this->getAddRoute($cached_values), $route_parameters);
}
public function add(array &$form, FormStateInterface $form_state) {
$condition = $form_state->getValue('conditions');
$content = \Drupal::formBuilder()->getForm($this->getConditionClass(), $condition, $this->getTempstoreId(), $this->machine_name);
$content['#attached']['library'][] = 'core/drupal.dialog.ajax';
$cached_values = $form_state->getTemporaryValue('wizard');
list(, $route_parameters) = $this->getOperationsRouteInfo($cached_values, $this->machine_name, $form_state->getValue('conditions'));
$content['submit']['#attached']['drupalSettings']['ajax'][$content['submit']['#id']]['url'] = $this->url($this->getAddRoute($cached_values), $route_parameters, ['query' => [FormBuilderInterface::AJAX_FORM_REQUEST => TRUE]]);
$response = new AjaxResponse();
$response->addCommand(new OpenModalDialogCommand($this->t('Configure Required Context'), $content, array('width' => '700')));
return $response;
}
/**
* @param $cached_values
*
* @return array
*/
public function renderRows($cached_values) {
$configured_conditions = array();
foreach ($this->getConditions($cached_values) as $row => $condition) {
/** @var $instance \Drupal\Core\Condition\ConditionInterface */
$instance = $this->manager->createInstance($condition['id'], $condition);
list($route_name, $route_parameters) = $this->getOperationsRouteInfo($cached_values, $cached_values['id'], $row);
$build = array(
'#type' => 'operations',
'#links' => $this->getOperations($route_name, $route_parameters),
);
$configured_conditions[] = array(
$instance->getPluginId(),
$instance->summary(),
'operations' => [
'data' => $build,
],
);
}
return $configured_conditions;
}
protected function getOperations($route_name_base, array $route_parameters = array()) {
$operations['edit'] = array(
'title' => $this->t('Edit'),
'url' => new Url($route_name_base . '.edit', $route_parameters),
'weight' => 10,
'attributes' => array(
'class' => ['use-ajax'],
'data-dialog-type' => 'modal',
'data-dialog-options' => Json::encode([
'width' => 700,
]),
),
);
$route_parameters['id'] = $route_parameters['condition'];
$operations['delete'] = array(
'title' => $this->t('Delete'),
'url' => new Url($route_name_base . '.delete', $route_parameters),
'weight' => 100,
'attributes' => array(
'class' => array('use-ajax'),
'data-dialog-type' => 'modal',
'data-dialog-options' => Json::encode([
'width' => 700,
]),
),
);
return $operations;
}
/**
* Return a subclass of '\Drupal\ctools\Form\ConditionConfigure'.
*
* The ConditionConfigure class is designed to be subclassed with custom
* route information to control the modal/redirect needs of your use case.
*
* @return string
*/
abstract protected function getConditionClass();
/**
* The route to which condition 'add' actions should submit.
*
* @param mixed $cached_values
*
* @return string
*/
abstract protected function getAddRoute($cached_values);
/**
* Provide the tempstore id for your specified use case.
*
* @return string
*/
abstract protected function getTempstoreId();
/**
* Document the route name and parameters for edit/delete context operations.
*
* The route name returned from this method is used as a "base" to which
* ".edit" and ".delete" are appeneded in the getOperations() method.
* Subclassing '\Drupal\ctools\Form\ConditionConfigure' and
* '\Drupal\ctools\Form\ConditionDelete' should set you up for using this
* approach quite seamlessly.
*
* @param mixed $cached_values
*
* @param string $machine_name
*
* @param string $row
*
* @return array
* In the format of
* return ['route.base.name', ['machine_name' => $machine_name, 'context' => $row]];
*/
abstract protected function getOperationsRouteInfo($cached_values, $machine_name, $row);
/**
* Custom logic for retrieving the conditions array from cached_values.
*
* @param $cached_values
*
* @return array
*/
abstract protected function getConditions($cached_values);
/**
* Custom logic for retrieving the contexts array from cached_values.
*
* @param $cached_values
*
* @return \Drupal\Core\Plugin\Context\ContextInterface[]
*/
abstract protected function getContexts($cached_values);
}

View file

@ -0,0 +1,327 @@
<?php
namespace Drupal\ctools\Form;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\OpenModalDialogCommand;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\TypedData\TypedDataManagerInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
abstract class ManageContext extends FormBase {
/**
* The machine name of the wizard we're working with.
*
* @var string
*/
protected $machine_name;
/**
* The typed data manager.
*
* @var \Drupal\Core\TypedData\TypedDataManagerInterface
*/
protected $typedDataManager;
/**
* The form builder.
*
* @var \Drupal\Core\Form\FormBuilderInterface
*/
protected $formBuilder;
/**
* An array of property types that are eligible as relationships.
*
* @var array
*/
protected $property_types = [];
/**
* A property for controlling usage of relationships in an implementation.
*
* @var bool
*/
protected $relationships = TRUE;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static($container->get('typed_data_manager'), $container->get('form_builder'));
}
/**
* ManageContext constructor.
*
* @param \Drupal\Core\TypedData\TypedDataManagerInterface $typed_data_manager
* The typed data manager.
* @param \Drupal\Core\Form\FormBuilderInterface $form_builder
* The form builder.
*/
public function __construct(TypedDataManagerInterface $typed_data_manager, FormBuilderInterface $form_builder) {
$this->typedDataManager = $typed_data_manager;
$this->formBuilder = $form_builder;
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'ctools_manage_context_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$cached_values = $form_state->getTemporaryValue('wizard');
$this->machine_name = $cached_values['id'];
$form['items'] = array(
'#type' => 'markup',
'#prefix' => '<div id="configured-contexts">',
'#suffix' => '</div>',
'#theme' => 'table',
'#header' => array($this->t('Context ID'), $this->t('Label'), $this->t('Data Type'), $this->t('Options')),
'#rows' => $this->renderRows($cached_values),
'#empty' => $this->t('No contexts or relationships have been added.')
);
foreach ($this->typedDataManager->getDefinitions() as $type => $definition) {
$types[$type] = $definition['label'];
}
if (isset($types['entity'])) {
unset($types['entity']);
}
asort($types);
$form['context'] = [
'#type' => 'select',
'#options' => $types,
];
$form['add'] = [
'#type' => 'submit',
'#name' => 'add',
'#value' => $this->t('Add new context'),
'#ajax' => [
'callback' => [$this, 'addContext'],
'event' => 'click',
],
'#submit' => [
'callback' => [$this, 'submitForm'],
]
];
$form['relationships'] = [
'#type' => 'select',
'#title' => $this->t('Add a relationship'),
'#options' => $this->getAvailableRelationships($cached_values),
'#access' => $this->relationships,
];
$form['add_relationship'] = [
'#type' => 'submit',
'#name' => 'add_relationship',
'#value' => $this->t('Add Relationship'),
'#ajax' => [
'callback' => [$this, 'addRelationship'],
'event' => 'click',
],
'#submit' => [
'callback' => [$this, 'submitForm'],
],
'#access' => $this->relationships,
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
if ($form_state->getTriggeringElement()['#name'] == 'add') {
$cached_values = $form_state->getTemporaryValue('wizard');
list(, $route_parameters) = $this->getContextOperationsRouteInfo($cached_values, $this->machine_name, $form_state->getValue('context'));
$form_state->setRedirect($this->getContextAddRoute($cached_values), $route_parameters);
}
if ($form_state->getTriggeringElement()['#name'] == 'add_relationship') {
$cached_values = $form_state->getTemporaryValue('wizard');
list(, $route_parameters) = $this->getRelationshipOperationsRouteInfo($cached_values, $this->machine_name, $form_state->getValue('relationships'));
$form_state->setRedirect($this->getRelationshipAddRoute($cached_values), $route_parameters);
}
}
public function addContext(array &$form, FormStateInterface $form_state) {
$context = $form_state->getValue('context');
$content = $this->formBuilder->getForm($this->getContextClass(), $context, $this->getTempstoreId(), $this->machine_name);
$content['#attached']['library'][] = 'core/drupal.dialog.ajax';
$cached_values = $form_state->getTemporaryValue('wizard');
list(, $route_parameters) = $this->getContextOperationsRouteInfo($cached_values, $this->machine_name, $context);
$content['submit']['#attached']['drupalSettings']['ajax'][$content['submit']['#id']]['url'] = $this->url($this->getContextAddRoute($cached_values), $route_parameters, ['query' => [FormBuilderInterface::AJAX_FORM_REQUEST => TRUE]]);
$response = new AjaxResponse();
$response->addCommand(new OpenModalDialogCommand($this->t('Add new context'), $content, array('width' => '700')));
return $response;
}
public function addRelationship(array &$form, FormStateInterface $form_state) {
$relationship = $form_state->getValue('relationships');
$content = $this->formBuilder->getForm($this->getRelationshipClass(), $relationship, $this->getTempstoreId(), $this->machine_name);
$content['#attached']['library'][] = 'core/drupal.dialog.ajax';
$cached_values = $form_state->getTemporaryValue('wizard');
list(, $route_parameters) = $this->getRelationshipOperationsRouteInfo($cached_values, $this->machine_name, $relationship);
$content['submit']['#attached']['drupalSettings']['ajax'][$content['submit']['#id']]['url'] = $this->url($this->getRelationshipAddRoute($cached_values), $route_parameters, ['query' => [FormBuilderInterface::AJAX_FORM_REQUEST => TRUE]]);
$response = new AjaxResponse();
$response->addCommand(new OpenModalDialogCommand($this->t('Configure Relationship'), $content, array('width' => '700')));
return $response;
}
protected function getAvailableRelationships($cached_values) {
/** @var \Drupal\ctools\TypedDataResolver $resolver */
$resolver = \Drupal::service('ctools.typed_data.resolver');
return $resolver->getTokensForContexts($this->getContexts($cached_values));
}
/**
* @param $cached_values
*
* @return array
*/
protected function renderRows($cached_values) {
$contexts = array();
foreach ($this->getContexts($cached_values) as $row => $context) {
list($route_name, $route_parameters) = $this->getContextOperationsRouteInfo($cached_values, $this->machine_name, $row);
$build = array(
'#type' => 'operations',
'#links' => $this->getOperations($cached_values, $row, $route_name, $route_parameters),
);
$contexts[$row] = array(
$row,
$context->getContextDefinition()->getLabel(),
$context->getContextDefinition()->getDataType(),
'operations' => [
'data' => $build,
],
);
}
return $contexts;
}
/**
* @param array $cached_values
* @param string $row
* @param string $route_name_base
* @param array $route_parameters
*
* @return mixed
*/
protected function getOperations($cached_values, $row, $route_name_base, array $route_parameters = array()) {
$operations = [];
if ($this->isEditableContext($cached_values, $row)) {
$operations['edit'] = array(
'title' => $this->t('Edit'),
'url' => new Url($route_name_base . '.edit', $route_parameters),
'weight' => 10,
'attributes' => array(
'class' => ['use-ajax'],
'data-dialog-type' => 'modal',
'data-dialog-options' => Json::encode([
'width' => 700,
]),
),
);
$operations['delete'] = array(
'title' => $this->t('Delete'),
'url' => new Url($route_name_base . '.delete', $route_parameters),
'weight' => 100,
'attributes' => array(
'class' => array('use-ajax'),
'data-dialog-type' => 'modal',
'data-dialog-options' => Json::encode([
'width' => 700,
]),
),
);
}
return $operations;
}
/**
* Return a subclass of '\Drupal\ctools\Form\ContextConfigure'.
*
* The ContextConfigure class is designed to be subclassed with custom
* route information to control the modal/redirect needs of your use case.
*
* @return string
*/
abstract protected function getContextClass($cached_values);
/**
* Return a subclass of '\Drupal\ctools\Form\RelationshipConfigure'.
*
* The RelationshipConfigure class is designed to be subclassed with custom
* route information to control the modal/redirect needs of your use case.
*
* @return string
*/
abstract protected function getRelationshipClass($cached_values);
/**
* The route to which context 'add' actions should submit.
*
* @return string
*/
abstract protected function getContextAddRoute($cached_values);
/**
* The route to which relationship 'add' actions should submit.
*
* @return string
*/
abstract protected function getRelationshipAddRoute($cached_values);
/**
* Provide the tempstore id for your specified use case.
*
* @return string
*/
abstract protected function getTempstoreId();
/**
* Returns the contexts already available in the wizard.
*
* @param mixed $cached_values
*
* @return \Drupal\Core\Plugin\Context\ContextInterface[]
*/
abstract protected function getContexts($cached_values);
/**
* @param mixed $cached_values
* @param string $machine_name
* @param string $row
*
* @return array
*/
abstract protected function getContextOperationsRouteInfo($cached_values, $machine_name, $row);
/**
* @param mixed $cached_values
* @param string $machine_name
* @param string $row
*
* @return array
*/
abstract protected function getRelationshipOperationsRouteInfo($cached_values, $machine_name, $row);
/**
* @param mixed $cached_values
* @param string $row
*
* @return bool
*/
abstract protected function isEditableContext($cached_values, $row);
}

View file

@ -0,0 +1,205 @@
<?php
namespace Drupal\ctools\Form;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\OpenModalDialogCommand;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
abstract class ManageResolverRelationships extends FormBase {
/**
* @var string
*/
protected $machine_name;
/**
* An array of property types that are eligible as relationships.
*
* @var array
*/
protected $property_types = [];
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'ctools_manage_resolver_relationships_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$cached_values = $form_state->getTemporaryValue('wizard');
$this->machine_name = $cached_values['id'];
$form['items'] = array(
'#type' => 'markup',
'#prefix' => '<div id="configured-relationships">',
'#suffix' => '</div>',
'#theme' => 'table',
'#header' => array($this->t('Context ID'), $this->t('Label'), $this->t('Data Type'), $this->t('Options')),
'#rows' => $this->renderRows($cached_values),
'#empty' => $this->t('No relationships have been added.')
);
$form['relationships'] = [
'#type' => 'select',
'#title' => $this->t('Add a relationship'),
'#options' => $this->getAvailableRelationships($cached_values),
];
$form['add_relationship'] = [
'#type' => 'submit',
'#name' => 'add',
'#value' => $this->t('Add Relationship'),
'#ajax' => [
'callback' => [$this, 'addRelationship'],
'event' => 'click',
],
'#submit' => [
'callback' => [$this, 'submitForm'],
]
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
if ($form_state->getTriggeringElement()['#name'] == 'add') {
$cached_values = $form_state->getTemporaryValue('wizard');
list(, $route_parameters) = $this->getRelationshipOperationsRouteInfo($cached_values, $this->machine_name, $form_state->getValue('relationships'));
$form_state->setRedirect($this->getAddRoute($cached_values), $route_parameters);
}
}
public function addRelationship(array &$form, FormStateInterface $form_state) {
$relationship = $form_state->getValue('relationships');
$content = \Drupal::formBuilder()->getForm($this->getContextClass(), $relationship, $this->getTempstoreId(), $this->machine_name);
$content['#attached']['library'][] = 'core/drupal.dialog.ajax';
$cached_values = $form_state->getTemporaryValue('wizard');
list(, $route_parameters) = $this->getRelationshipOperationsRouteInfo($cached_values, $this->machine_name, $relationship);
$content['submit']['#attached']['drupalSettings']['ajax'][$content['submit']['#id']]['url'] = $this->url($this->getAddRoute($cached_values), $route_parameters, ['query' => [FormBuilderInterface::AJAX_FORM_REQUEST => TRUE]]);
$response = new AjaxResponse();
$response->addCommand(new OpenModalDialogCommand($this->t('Configure Relationship'), $content, array('width' => '700')));
return $response;
}
protected function getAvailableRelationships($cached_values) {
/** @var \Drupal\ctools\TypedDataResolver $resolver */
$resolver = \Drupal::service('ctools.typed_data.resolver');
return $resolver->getTokensForContexts($this->getContexts($cached_values));
}
/**
* @param $cached_values
*
* @return array
*/
protected function renderRows($cached_values) {
$contexts = array();
foreach ($this->getContexts($cached_values) as $row => $context) {
list($route_name, $route_parameters) = $this->getRelationshipOperationsRouteInfo($cached_values, $this->machine_name, $row);
$build = array(
'#type' => 'operations',
'#links' => $this->getOperations($cached_values, $row, $route_name, $route_parameters),
);
$contexts[$row] = array(
$row,
$context->getContextDefinition()->getLabel(),
$context->getContextDefinition()->getDataType(),
'operations' => [
'data' => $build,
],
);
}
return $contexts;
}
/**
* @param array $cached_values
* @param string $row
* @param string $route_name_base
* @param array $route_parameters
*
* @return mixed
*/
protected function getOperations($cached_values, $row, $route_name_base, array $route_parameters = array()) {
// Base contexts will not be a : separated and generated relationships should have 3 parts.
if (count(explode(':', $row)) < 2) {
return [];
}
$operations['edit'] = array(
'title' => $this->t('Edit'),
'url' => new Url($route_name_base . '.edit', $route_parameters),
'weight' => 10,
'attributes' => array(
'class' => ['use-ajax'],
'data-dialog-type' => 'modal',
'data-dialog-options' => Json::encode([
'width' => 700,
]),
),
);
$route_parameters['id'] = $route_parameters['context'];
$operations['delete'] = array(
'title' => $this->t('Delete'),
'url' => new Url($route_name_base . '.delete', $route_parameters),
'weight' => 100,
'attributes' => array(
'class' => array('use-ajax'),
'data-dialog-type' => 'modal',
'data-dialog-options' => Json::encode([
'width' => 700,
]),
),
);
return $operations;
}
/**
* Return a subclass of '\Drupal\ctools\Form\ResolverRelationshipConfigure'.
*
* The ConditionConfigure class is designed to be subclassed with custom
* route information to control the modal/redirect needs of your use case.
*
* @return string
*/
abstract protected function getContextClass($cached_values);
/**
* The route to which relationship 'add' actions should submit.
*
* @return string
*/
abstract protected function getAddRoute($cached_values);
/**
* Provide the tempstore id for your specified use case.
*
* @return string
*/
abstract protected function getTempstoreId();
/**
* @param $cached_values
*
* @return \Drupal\Core\Plugin\Context\ContextInterface[]
*/
abstract protected function getContexts($cached_values);
/**
* @param string $cached_values
* @param string $machine_name
* @param string $row
*
* @return array
*/
abstract protected function getRelationshipOperationsRouteInfo($cached_values, $machine_name, $row);
}

View file

@ -0,0 +1,152 @@
<?php
namespace Drupal\ctools\Form;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\CloseModalDialogCommand;
use Drupal\Core\Ajax\RedirectCommand;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\ctools\TypedDataResolver;
use Drupal\user\SharedTempStoreFactory;
use Symfony\Component\DependencyInjection\ContainerInterface;
abstract class RelationshipConfigure extends FormBase {
/**
* @var \Drupal\user\SharedTempStoreFactory
*/
protected $tempstore;
/**
* @var \Drupal\ctools\TypedDataResolver
*/
protected $resolver;
/**
* @var string
*/
protected $tempstore_id;
/**
* @var string;
*/
protected $machine_name;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static($container->get('user.shared_tempstore'), $container->get('ctools.typed_data.resolver'));
}
public function __construct(SharedTempStoreFactory $tempstore, TypedDataResolver $resolver) {
$this->tempstore = $tempstore;
$this->resolver = $resolver;
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'ctools_relationship_configure';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, $context_id = NULL, $tempstore_id = NULL, $machine_name = NULL) {
$this->tempstore_id = $tempstore_id;
$this->machine_name = $machine_name;
$cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name);
/** @var \Drupal\Core\Plugin\Context\ContextInterface[] $contexts */
$contexts = $this->getContexts($cached_values);
$context_object = $this->resolver->convertTokenToContext($context_id, $contexts);
$form['id'] = [
'#type' => 'value',
'#value' => $context_id
];
$form['context_object'] = [
'#type' => 'value',
'#value' => $context_object,
];
$form['label'] = [
'#type' => 'textfield',
'#title' => $this->t('Context label'),
'#default_value' => !empty($contexts[$context_id]) ? $contexts[$context_id]->getContextDefinition()->getLabel() : $this->resolver->getLabelByToken($context_id, $contexts),
'#required' => TRUE,
];
$form['context_data'] = [
'#type' => 'item',
'#title' => $this->t('Data type'),
'#markup' => $context_object->getContextDefinition()->getDataType(),
];
$form['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Save'),
'#ajax' => [
'callback' => [$this, 'ajaxSave'],
]
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name);
list($route_name, $route_options) = $this->getParentRouteInfo($cached_values);
$form_state->setRedirect($route_name, $route_options);
}
/**
* @param array $form
* @param \Drupal\Core\Form\FormStateInterface $form_state
*
* @return \Drupal\Core\Ajax\AjaxResponse
*/
public function ajaxSave(array &$form, FormStateInterface $form_state) {
$cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name);
list($route_name, $route_parameters) = $this->getParentRouteInfo($cached_values);
$response = new AjaxResponse();
$response->addCommand(new RedirectCommand($this->url($route_name, $route_parameters)));
$response->addCommand(new CloseModalDialogCommand());
return $response;
}
/**
* Document the route name and parameters for redirect after submission.
*
* @param array $cached_values
*
* @return array In the format of
* In the format of
* return ['route.name', ['machine_name' => $this->machine_name, 'step' => 'step_name']];
*/
abstract protected function getParentRouteInfo($cached_values);
/**
* Custom logic for setting the conditions array in cached_values.
*
* @param $cached_values
*
* @param $contexts
* The conditions to set within the cached values.
*
* @return mixed
* Return the $cached_values
*/
abstract protected function setContexts($cached_values, $contexts);
/**
* Custom logic for retrieving the contexts array from cached_values.
*
* @param $cached_values
*
* @return \Drupal\Core\Plugin\Context\ContextInterface[]
*/
abstract protected function getContexts($cached_values);
}

View file

@ -0,0 +1,212 @@
<?php
namespace Drupal\ctools\Form;
use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\OpenModalDialogCommand;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
abstract class RequiredContext extends FormBase {
/**
* @var \Drupal\Core\TypedData\TypedDataManager
*/
protected $typedDataManager;
/**
* @var string
*/
protected $machine_name;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static($container->get('typed_data_manager'));
}
public function __construct(PluginManagerInterface $typed_data_manager) {
$this->typedDataManager = $typed_data_manager;
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'ctools_required_context_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$cached_values = $form_state->getTemporaryValue('wizard');
$this->machine_name = $cached_values['id'];
$form['#attached']['library'][] = 'core/drupal.dialog.ajax';
$options = [];
foreach ($this->typedDataManager->getDefinitions() as $plugin_id => $definition) {
$options[$plugin_id] = (string) $definition['label'];
}
$form['items'] = array(
'#type' => 'markup',
'#prefix' => '<div id="configured-contexts">',
'#suffix' => '</div>',
'#theme' => 'table',
'#header' => array($this->t('Information'), $this->t('Description'), $this->t('Operations')),
'#rows' => $this->renderContexts($cached_values),
'#empty' => $this->t('No required contexts have been configured.')
);
$form['contexts'] = [
'#type' => 'select',
'#options' => $options,
];
$form['add'] = [
'#type' => 'submit',
'#name' => 'add',
'#value' => $this->t('Add required context'),
'#ajax' => [
'callback' => [$this, 'add'],
'event' => 'click',
],
'#submit' => [
'callback' => [$this, 'submitform'],
]
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$cached_values = $form_state->getTemporaryValue('wizard');
list($route_name, $route_parameters) = $this->getOperationsRouteInfo($cached_values, $this->machine_name, $form_state->getValue('contexts'));
$form_state->setRedirect($route_name . '.edit', $route_parameters);
}
/**
* Custom ajax form submission handler.
*
* @param array $form
* @param \Drupal\Core\Form\FormStateInterface $form_state
*
* @return \Drupal\Core\Ajax\AjaxResponse
*/
public function add(array &$form, FormStateInterface $form_state) {
$context = $form_state->getValue('contexts');
$content = \Drupal::formBuilder()->getForm($this->getContextClass(), $context, $this->getTempstoreId(), $this->machine_name);
$content['#attached']['library'][] = 'core/drupal.dialog.ajax';
$response = new AjaxResponse();
$response->addCommand(new OpenModalDialogCommand($this->t('Configure Required Context'), $content, array('width' => '700')));
return $response;
}
/**
* @param $cached_values
*
* @return array
*/
public function renderContexts($cached_values) {
$configured_contexts = array();
foreach ($this->getContexts($cached_values) as $row => $context) {
list($plugin_id, $label, $machine_name, $description) = array_values($context);
list($route_name, $route_parameters) = $this->getOperationsRouteInfo($cached_values, $cached_values['id'], $row);
$build = array(
'#type' => 'operations',
'#links' => $this->getOperations($route_name, $route_parameters),
);
$configured_contexts[] = array(
$this->t('<strong>Label:</strong> @label<br /> <strong>Type:</strong> @type', ['@label' => $label, '@type' => $plugin_id]),
$this->t('@description', ['@description' => $description]),
'operations' => [
'data' => $build,
],
);
}
return $configured_contexts;
}
protected function getOperations($route_name_base, array $route_parameters = array()) {
$operations['edit'] = array(
'title' => $this->t('Edit'),
'url' => new Url($route_name_base . '.edit', $route_parameters),
'weight' => 10,
'attributes' => array(
'class' => array('use-ajax'),
'data-accepts' => 'application/vnd.drupal-modal',
'data-dialog-options' => json_encode(array(
'width' => 700,
)),
),
'ajax' => [
''
],
);
$route_parameters['id'] = $route_parameters['context'];
$operations['delete'] = array(
'title' => $this->t('Delete'),
'url' => new Url($route_name_base . '.delete', $route_parameters),
'weight' => 100,
'attributes' => array(
'class' => array('use-ajax'),
'data-accepts' => 'application/vnd.drupal-modal',
'data-dialog-options' => json_encode(array(
'width' => 700,
)),
),
);
return $operations;
}
/**
* Return a subclass of '\Drupal\ctools\Form\ContextConfigure'.
*
* The ContextConfigure class is designed to be subclassed with custom route
* information to control the modal/redirect needs of your use case.
*
* @return string
*/
abstract protected function getContextClass();
/**
* Provide the tempstore id for your specified use case.
*
* @return string
*/
abstract protected function getTempstoreId();
/**
* Document the route name and parameters for edit/delete context operations.
*
* The route name returned from this method is used as a "base" to which
* ".edit" and ".delete" are appeneded in the getOperations() method.
* Subclassing '\Drupal\ctools\Form\ContextConfigure' and
* '\Drupal\ctools\Form\RequiredContextDelete' should set you up for using
* this approach quite seamlessly.
*
* @param mixed $cached_values
*
* @param string $machine_name
*
* @param string $row
*
* @return array
* In the format of
* return ['route.base.name', ['machine_name' => $machine_name, 'context' => $row]];
*/
abstract protected function getOperationsRouteInfo($cached_values, $machine_name, $row);
/**
* Custom logic for retrieving the contexts array from cached_values.
*
* @param $cached_values
*
* @return array
*/
abstract protected function getContexts($cached_values);
}

View file

@ -0,0 +1,194 @@
<?php
namespace Drupal\ctools\Form;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\ConfirmFormHelper;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Drupal\user\SharedTempStoreFactory;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Base class for adding a required contexts step to your wizard.
*/
abstract class RequiredContextDelete extends ConfirmFormBase {
/**
* @var \Drupal\user\SharedTempStoreFactory
*/
protected $tempstore;
/**
* @var string
*/
protected $tempstore_id;
/**
* @var string;
*/
protected $machine_name;
/**
* @var int;
*/
protected $id;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static($container->get('user.shared_tempstore'));
}
/**
* @param \Drupal\user\SharedTempStoreFactory $tempstore
*/
function __construct(SharedTempStoreFactory $tempstore) {
$this->tempstore = $tempstore;
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'ctools_required_context_delete';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, $id = NULL, $tempstore_id = NULL, $machine_name = NULL) {
$this->tempstore_id = $tempstore_id;
$this->machine_name = $machine_name;
$this->id = $id;
$cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name);
$form ['#title'] = $this->getQuestion($id, $cached_values);
$form ['#attributes']['class'][] = 'confirmation';
$form ['description'] = array('#markup' => $this->getDescription());
$form [$this->getFormName()] = array('#type' => 'hidden', '#value' => 1);
// By default, render the form using theme_confirm_form().
if (!isset($form ['#theme'])) {
$form ['#theme'] = 'confirm_form';
}
$form['actions'] = array('#type' => 'actions');
$form['actions'] += $this->actions($form, $form_state);
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name);
$contexts = $this->getContexts($cached_values);
unset($contexts[$this->id]);
$cached_values = $this->setContexts($cached_values, $contexts);
$this->tempstore->get($this->tempstore_id)->set($this->machine_name, $cached_values);
list($route_name, $route_parameters) = $this->getParentRouteInfo($cached_values);
$form_state->setRedirect($route_name, $route_parameters);
}
/**
* {@inheritdoc}
*/
public function getQuestion($id = NULL, $cached_values = NULL) {
$context = $this->getContexts($cached_values)[$id];
return $this->t('Are you sure you want to delete the @label context?', array(
'@label' => $context['label'],
));
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return $this->t('This action cannot be undone.');
}
/**
* {@inheritdoc}
*/
public function getFormName() {
return 'confirm';
}
/**
* Provides the action buttons for submitting this form.
*/
protected function actions(array $form, FormStateInterface $form_state) {
return array(
'submit' => array(
'#type' => 'submit',
'#value' => $this->getConfirmText(),
'#validate' => array(
array($this, 'validate'),
),
'#submit' => array(
array($this, 'submitForm'),
),
),
'cancel' => ConfirmFormHelper::buildCancelLink($this, $this->getRequest()),
);
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
$cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name);
list($route_name, $route_parameters) = $this->getParentRouteInfo($cached_values);
return new Url($route_name, $route_parameters);
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Delete');
}
/**
* {@inheritdoc}
*/
public function getCancelText() {
return $this->t('Cancel');
}
/**
* Document the route name and parameters for redirect after submission.
*
* @param $cached_values
*
* @return array
* In the format of
* return ['route.name', ['machine_name' => $this->machine_name, 'step' => 'step_name]];
*/
abstract protected function getParentRouteInfo($cached_values);
/**
* Custom logic for retrieving the contexts array from cached_values.
*
* @param $cached_values
*
* @return array
*/
abstract protected function getContexts($cached_values);
/**
* Custom logic for setting the contexts array in cached_values.
*
* @param $cached_values
*
* @param $contexts
* The contexts to set within the cached values.
*
* @return mixed
* Return the $cached_values
*/
abstract protected function setContexts($cached_values, $contexts);
}

View file

@ -0,0 +1,182 @@
<?php
namespace Drupal\ctools\Form;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\CloseModalDialogCommand;
use Drupal\Core\Ajax\RedirectCommand;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\user\SharedTempStoreFactory;
use Symfony\Component\DependencyInjection\ContainerInterface;
abstract class ResolverRelationshipConfigure extends FormBase {
/**
* @var \Drupal\user\SharedTempStoreFactory
*/
protected $tempstore;
/**
* @var string
*/
protected $tempstore_id;
/**
* @var string;
*/
protected $machine_name;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static($container->get('user.shared_tempstore'));
}
function __construct(SharedTempStoreFactory $tempstore) {
$this->tempstore = $tempstore;
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'ctools_context_configure';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, $context = NULL, $tempstore_id = NULL, $machine_name = NULL) {
$this->tempstore_id = $tempstore_id;
$this->machine_name = $machine_name;
$cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name);
if (is_numeric($context)) {
$id = $context;
$contexts = $this->getContexts($cached_values);
$context = $contexts[$id]['context'];
$label = $contexts[$id]['label'];
$machine_name = $contexts[$id]['machine_name'];
$description = $contexts[$id]['description'];
// Conditionally set this form element so that we can update or add.
$form['id'] = [
'#type' => 'value',
'#value' => $id
];
}
else {
$label = '';
$machine_name = '';
$description = '';
}
$form['#attached']['library'][] = 'core/drupal.dialog.ajax';
$form['context'] = [
'#type' => 'value',
'#value' => $context
];
$form['label'] = [
'#type' => 'textfield',
'#title' => $this->t('Label'),
'#default_value' => $label,
'#required' => TRUE,
];
$form['machine_name'] = [
'#type' => 'textfield',
'#title' => $this->t('Machine Name'),
'#default_value' => $machine_name,
'#required' => TRUE,
];
$form['description'] = [
'#type' => 'textarea',
'#title' => $this->t('Description'),
'#default_value' => $description,
];
$form['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Save'),
'#ajax' => [
'callback' => [$this, 'ajaxSave'],
]
];
return $form;
}
public function validateForm(array &$form, FormStateInterface $form_state) {
$machine_name = $form_state->getValue('machine_name');
$cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name);
foreach ($this->getContexts($cached_values) as $id => $context) {
if ($context['machine_name'] == $machine_name) {
$form_state->setError($form['machine_name'], $this->t('That machine name is in use by another context definition.'));
}
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name);
$contexts = $this->getContexts($cached_values);
$context = [
'context' => $form_state->getValue('context'),
'label' => $form_state->getValue('label'),
'machine_name' => $form_state->getValue('machine_name'),
'description' => $form_state->getValue('description'),
];
if ($form_state->hasValue('id')) {
$contexts[$form_state->getValue('id')] = $context;
}
else {
$contexts[] = $context;
}
$cached_values = $this->setContexts($cached_values, $contexts);
$this->tempstore->get($this->tempstore_id)->set($this->machine_name, $cached_values);
list($route_name, $route_parameters) = $this->getParentRouteInfo($cached_values);
$form_state->setRedirect($route_name, $route_parameters);
}
public function ajaxSave(array &$form, FormStateInterface $form_state) {
$response = new AjaxResponse();
$cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name);
list($route_name, $route_parameters) = $this->getParentRouteInfo($cached_values);
$response->addCommand(new RedirectCommand($this->url($route_name, $route_parameters)));
$response->addCommand(new CloseModalDialogCommand());
return $response;
}
/**
* Document the route name and parameters for redirect after submission.
*
* @param $cached_values
*
* @return array
* In the format of
* return ['route.name', ['machine_name' => $this->machine_name, 'step' => 'step_name]];
*/
abstract protected function getParentRouteInfo($cached_values);
/**
* Custom logic for retrieving the contexts array from cached_values.
*
* @param $cached_values
*
* @return array
*/
abstract protected function getContexts($cached_values);
/**
* Custom logic for setting the contexts array in cached_values.
*
* @param $cached_values
*
* @param $contexts
* The contexts to set within the cached values.
*
* @return mixed
* Return the $cached_values
*/
abstract protected function setContexts($cached_values, $contexts);
}

View file

@ -0,0 +1,150 @@
<?php
namespace Drupal\ctools\Form;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\ConfirmFormHelper;
use Drupal\Core\Form\FormStateInterface;
use Drupal\ctools\TypedDataResolver;
use Drupal\user\SharedTempStoreFactory;
use Symfony\Component\DependencyInjection\ContainerInterface;
abstract class ResolverRelationshipDelete extends ConfirmFormBase {
/**
* @var \Drupal\user\SharedTempStoreFactory
*/
protected $tempstore;
/**
* @var \Drupal\ctools\TypedDataResolver
*/
protected $resolver;
/**
* @var string
*/
protected $tempstore_id;
/**
* @var string;
*/
protected $machine_name;
/**
* @var string;
*/
protected $id;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static($container->get('user.shared_tempstore'), $container->get('ctools.typed_data.resolver'));
}
/**
* @param \Drupal\user\SharedTempStoreFactory $tempstore
* The shared tempstore.
* @param \Drupal\ctools\TypedDataResolver $resolver
* The the typed data resolver.
*/
public function __construct(SharedTempStoreFactory $tempstore, TypedDataResolver $resolver) {
$this->tempstore = $tempstore;
$this->resolver = $resolver;
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'ctools_resolver_relationship_delete';
}
/**
* {@inheritdoc}
*/
public function getQuestion($id = NULL, $cached_values = []) {
$context = $this->getContexts($cached_values)[$id];
return $this->t('Are you sure you want to delete the @label relationship?', [
'@label' => $context->getContextDefinition()->getLabel(),
]);
}
/**
* {@inheritdoc}
*/
abstract public function getCancelUrl($cached_values = []);
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, $id = NULL, $tempstore_id = NULL, $machine_name = NULL) {
$this->tempstore_id = $tempstore_id;
$this->machine_name = $machine_name;
$this->id = $id;
$cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name);
$form ['#title'] = $this->getQuestion($id, $cached_values);
$form ['#attributes']['class'][] = 'confirmation';
$form ['description'] = array('#markup' => $this->getDescription());
$form [$this->getFormName()] = array('#type' => 'hidden', '#value' => 1);
// By default, render the form using theme_confirm_form().
if (!isset($form ['#theme'])) {
$form ['#theme'] = 'confirm_form';
}
$form['actions'] = array('#type' => 'actions');
$form['actions'] += $this->actions($form, $form_state, $cached_values);
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name);
$form_state->setRedirectUrl($this->getCancelUrl($cached_values));
}
/**
* A custom form actions method.
*
* @param array $form
* The form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current form state.
* @param $cached_values
* The current wizard cached values.
*
* @return array
*/
protected function actions(array $form, FormStateInterface $form_state, $cached_values) {
return array(
'submit' => array(
'#type' => 'submit',
'#value' => $this->getConfirmText(),
'#validate' => array(
array($this, 'validate'),
),
'#submit' => array(
array($this, 'submitForm'),
),
),
'cancel' => ConfirmFormHelper::buildCancelLink($this, $this->getRequest()),
);
}
/**
* Extract contexts from the cached values.
*
* @param array $cached_values
* The cached values.
*
* @return \Drupal\Core\Plugin\Context\ContextInterface[]
*/
abstract public function getContexts($cached_values);
}

View file

@ -0,0 +1,169 @@
<?php
namespace Drupal\ctools\ParamConverter;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\ParamConverter\ParamConverterInterface;
use Drupal\user\SharedTempStoreFactory;
use Symfony\Component\Routing\Route;
/**
* Parameter converter for pulling entities out of the tempstore.
*
* This is particularly useful when building non-wizard forms (like dialogs)
* that operate on data in the wizard and getting the route access correct.
*
* There are four different ways to use this!
*
* In the most basic way, you specify the 'tempstore_id' in the defaults (so
* that the form/controller has access to it as well) and in the parameter type
* we simply give 'tempstore'. This assumes the entity is the full value
* returned from the tempstore.
*
* @code
* example.route:
* path: foo/{example}
* defaults:
* tempstore_id: example.foo
* options:
* parameters:
* example:
* type: tempstore
* @endcode
*
* If the value returned from the tempstore is an array, and the entity is
* one of the keys, then we specify that after 'tempstore:', for example:
*
* @code
* example.route:
* path: foo/{example}
* defaults:
* tempstore_id: example.foo
* options:
* parameters:
* example:
* # Get the 'foo' key from the array returned by the tempstore.
* type: tempstore:foo
* @endcode
*
* You can also specify the 'tempstore_id' under the parameter rather than in
* the defaults, for example:
*
* @code
* example.route:
* path: foo/{example}
* options:
* parameters:
* example:
* type: tempstore:foo
* tempstore_id: example.foo
* @endcode
*
* Or, if you have two parameters which are represented by two keys on the same
* array from the tempstore, put the slug which represents the id for the
* tempstore in the 2nd key. For example:
*
* @code
* example.route:
* path: foo/{example}/{other}
* defaults:
* tempstore_id: example.foo
* options:
* parameters:
* example:
* type: tempstore:foo
* other:
* type: tempstore:{example}:other
* @endcode
*/
class TempstoreConverter implements ParamConverterInterface {
/**
* The tempstore factory.
*
* @var \Drupal\user\SharedTempStoreFactory
*/
protected $tempstore;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a TempstoreConverter.
*
* @param \Drupal\user\SharedTempStoreFactory $tempstore
*/
public function __construct(SharedTempStoreFactory $tempstore, EntityTypeManagerInterface $entity_type_manager) {
$this->tempstore = $tempstore;
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public function convert($value, $definition, $name, array $defaults) {
$tempstore_id = !empty($definition['tempstore_id']) ? $definition['tempstore_id'] : $defaults['tempstore_id'];
$machine_name = $this->convertVariable($value, $defaults);
list(, $parts) = explode(':', $definition['type'], 2);
$parts = explode(':', $parts);
foreach ($parts as $key => $part) {
$parts[$key] = $this->convertVariable($part, $defaults);
}
$cached_values = $this->tempstore->get($tempstore_id)->get($machine_name);
// Entity type upcasting is most common, so we just assume that here.
// @todo see if there's a better way to do this.
if (!$cached_values && $this->entityTypeManager->hasDefinition($name)) {
$value = $this->entityTypeManager->getStorage($name)->load($machine_name);
return $value;
}
elseif (!$cached_values) {
return NULL;
}
else {
$value = NestedArray::getValue($cached_values, $parts, $key_exists);
return $key_exists ? $value : NULL;
}
}
/**
* A helper function for converting string variable names from the defaults.
*
* @param mixed $name
* If name is a string in the format of {var} it will parse the defaults
* for a 'var' default. If $name isn't a string or isn't a slug, it will
* return the raw $name value. If no default is found, it will return NULL
* @param array $defaults
* The route defaults array.
*
* @return mixed
* The value of a variable in defaults.
*/
protected function convertVariable($name, $defaults) {
if (is_string($name) && strpos($name, '{') === 0) {
$length = strlen($name);
$name = substr($name, 1, $length -2);
return isset($defaults[$name]) ? $defaults[$name] : NULL;
}
return $name;
}
/**
* {@inheritdoc}
*/
public function applies($definition, $name, Route $route) {
if (!empty($definition['type']) && ($definition['type'] == 'tempstore' || strpos($definition['type'], 'tempstore:') === 0)) {
if (!empty($definition['tempstore_id']) || $route->hasDefault('tempstore_id')) {
return TRUE;
}
}
return FALSE;
}
}

View file

@ -0,0 +1,105 @@
<?php
namespace Drupal\ctools\Plugin\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\ContextAwarePluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a block to view a specific entity.
*
* @Block(
* id = "entity_view",
* deriver = "Drupal\ctools\Plugin\Deriver\EntityViewDeriver",
* )
*/
class EntityView extends BlockBase implements ContextAwarePluginInterface, ContainerFactoryPluginInterface {
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* Constructs a new EntityView.
*
* @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\EntityManagerInterface $entity_manager
* The entity manager.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityManagerInterface $entity_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->entityManager = $entity_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity.manager')
);
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'view_mode' => 'default',
];
}
/**
* {@inheritdoc}
*/
public function blockForm($form, FormStateInterface $form_state) {
$form['view_mode'] = [
'#type' => 'select',
'#options' => $this->entityManager->getViewModeOptions($this->getDerivativeId()),
'#title' => $this->t('View mode'),
'#default_value' => $this->configuration['view_mode'],
];
return $form;
}
/**
* {@inheritdoc}
*/
public function blockSubmit($form, FormStateInterface $form_state) {
$this->configuration['view_mode'] = $form_state->getValue('view_mode');
}
/**
* {@inheritdoc}
*/
public function build() {
/** @var $entity \Drupal\Core\Entity\EntityInterface */
$entity = $this->getContextValue('entity');
$view_builder = $this->entityManager->getViewBuilder($entity->getEntityTypeId());
$build = $view_builder->view($entity, $this->configuration['view_mode']);
CacheableMetadata::createFromObject($this->getContext('entity'))
->applyTo($build);
return $build;
}
}

View file

@ -0,0 +1,53 @@
<?php
namespace Drupal\ctools\Plugin;
use Drupal\Core\Block\BlockPluginInterface;
use Drupal\Core\Plugin\DefaultLazyPluginCollection;
/**
* Provides a collection of block plugins.
*/
class BlockPluginCollection extends DefaultLazyPluginCollection {
/**
* {@inheritdoc}
*
* @return \Drupal\Core\Block\BlockPluginInterface
*/
public function &get($instance_id) {
return parent::get($instance_id);
}
/**
* Returns all blocks keyed by their region.
*
* @return array
* An associative array keyed by region, containing an associative array of
* block plugins.
*/
public function getAllByRegion() {
$region_assignments = [];
foreach ($this as $block_id => $block) {
$configuration = $block->getConfiguration();
$region = isset($configuration['region']) ? $configuration['region'] : NULL;
$region_assignments[$region][$block_id] = $block;
}
foreach ($region_assignments as $region => $region_assignment) {
// @todo Determine the reason this needs error suppression.
@uasort($region_assignment, function (BlockPluginInterface $a, BlockPluginInterface $b) {
$a_config = $a->getConfiguration();
$a_weight = isset($a_config['weight']) ? $a_config['weight'] : 0;
$b_config = $b->getConfiguration();
$b_weight = isset($b_config['weight']) ? $b_config['weight'] : 0;
if ($a_weight == $b_weight) {
return strcmp($a->label(), $b->label());
}
return $a_weight > $b_weight ? 1 : -1;
});
$region_assignments[$region] = $region_assignment;
}
return $region_assignments;
}
}

View file

@ -0,0 +1,96 @@
<?php
namespace Drupal\ctools\Plugin;
use Drupal\Core\Display\VariantInterface;
/**
* Provides an interface for variant plugins that use block plugins.
*/
interface BlockVariantInterface extends VariantInterface {
/**
* Returns the human-readable list of regions keyed by machine name.
*
* @return array
* An array of human-readable region names keyed by machine name.
*/
public function getRegionNames();
/**
* Returns the human-readable name of a specific region.
*
* @param string $region
* The machine name of a region.
*
* @return string
* The human-readable name of a region.
*/
public function getRegionName($region);
/**
* Adds a block to this display variant.
*
* @param array $configuration
* An array of block configuration.
*
* @return string
* The block ID.
*/
public function addBlock(array $configuration);
/**
* Returns the region a specific block is assigned to.
*
* @param string $block_id
* The block ID.
*
* @return string
* The machine name of the region this block is assigned to.
*/
public function getRegionAssignment($block_id);
/**
* Returns an array of regions and their block plugins.
*
* @return array
* The array is first keyed by region machine name, with the values
* containing an array keyed by block ID, with block plugin instances as the
* values.
*/
public function getRegionAssignments();
/**
* Returns a specific block plugin.
*
* @param string $block_id
* The block ID.
*
* @return \Drupal\Core\Block\BlockPluginInterface
* The block plugin.
*/
public function getBlock($block_id);
/**
* Updates the configuration of a specific block plugin.
*
* @param string $block_id
* The block ID.
* @param array $configuration
* The array of configuration to set.
*
* @return $this
*/
public function updateBlock($block_id, array $configuration);
/**
* Removes a specific block from this display variant.
*
* @param string $block_id
* The block ID.
*
* @return $this
*/
public function removeBlock($block_id);
}

View file

@ -0,0 +1,130 @@
<?php
namespace Drupal\ctools\Plugin;
/**
* Provides methods for \Drupal\ctools\Plugin\BlockVariantInterface.
*/
trait BlockVariantTrait {
/**
* The block manager.
*
* @var \Drupal\Core\Block\BlockManager
*/
protected $blockManager;
/**
* The plugin collection that holds the block plugins.
*
* @var \Drupal\ctools\Plugin\BlockPluginCollection
*/
protected $blockPluginCollection;
/**
* @see \Drupal\ctools\Plugin\BlockVariantInterface::getRegionNames()
*/
abstract public function getRegionNames();
/**
* @see \Drupal\ctools\Plugin\BlockVariantInterface::getBlock()
*/
public function getBlock($block_id) {
return $this->getBlockCollection()->get($block_id);
}
/**
* @see \Drupal\ctools\Plugin\BlockVariantInterface::addBlock()
*/
public function addBlock(array $configuration) {
$configuration['uuid'] = $this->uuidGenerator()->generate();
$this->getBlockCollection()->addInstanceId($configuration['uuid'], $configuration);
return $configuration['uuid'];
}
/**
* @see \Drupal\ctools\Plugin\BlockVariantInterface::removeBlock()
*/
public function removeBlock($block_id) {
$this->getBlockCollection()->removeInstanceId($block_id);
return $this;
}
/**
* @see \Drupal\ctools\Plugin\BlockVariantInterface::updateBlock()
*/
public function updateBlock($block_id, array $configuration) {
$existing_configuration = $this->getBlock($block_id)->getConfiguration();
$this->getBlockCollection()->setInstanceConfiguration($block_id, $configuration + $existing_configuration);
return $this;
}
/**
* @see \Drupal\ctools\Plugin\BlockVariantInterface::getRegionAssignment()
*/
public function getRegionAssignment($block_id) {
$configuration = $this->getBlock($block_id)->getConfiguration();
return isset($configuration['region']) ? $configuration['region'] : NULL;
}
/**
* @see \Drupal\ctools\Plugin\BlockVariantInterface::getRegionAssignments()
*/
public function getRegionAssignments() {
// Build an array of the region names in the right order.
$empty = array_fill_keys(array_keys($this->getRegionNames()), []);
$full = $this->getBlockCollection()->getAllByRegion();
// Merge it with the actual values to maintain the ordering.
return array_intersect_key(array_merge($empty, $full), $empty);
}
/**
* @see \Drupal\ctools\Plugin\BlockVariantInterface::getRegionName()
*/
public function getRegionName($region) {
$regions = $this->getRegionNames();
return isset($regions[$region]) ? $regions[$region] : '';
}
/**
* Gets the block plugin manager.
*
* @return \Drupal\Core\Block\BlockManager
* The block plugin manager.
*/
protected function getBlockManager() {
if (!$this->blockManager) {
$this->blockManager = \Drupal::service('plugin.manager.block');
}
return $this->blockManager;
}
/**
* Returns the block plugins used for this display variant.
*
* @return \Drupal\Core\Block\BlockPluginInterface[]|\Drupal\ctools\Plugin\BlockPluginCollection
* An array or collection of configured block plugins.
*/
protected function getBlockCollection() {
if (!$this->blockPluginCollection) {
$this->blockPluginCollection = new BlockPluginCollection($this->getBlockManager(), $this->getBlockConfig());
}
return $this->blockPluginCollection;
}
/**
* Returns the UUID generator.
*
* @return \Drupal\Component\Uuid\UuidInterface
*/
abstract protected function uuidGenerator();
/**
* Returns the configuration for stored blocks.
*
* @return array
* An array of block configuration, keyed by the unique block ID.
*/
abstract protected function getBlockConfig();
}

View file

@ -0,0 +1,160 @@
<?php
namespace Drupal\ctools\Plugin\Condition;
use Drupal\Core\Condition\ConditionPluginBase;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\ctools\ConstraintConditionInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a 'Entity Bundle' condition.
*
* @Condition(
* id = "entity_bundle",
* deriver = "\Drupal\ctools\Plugin\Deriver\EntityBundle"
* )
*
*/
class EntityBundle extends ConditionPluginBase implements ConstraintConditionInterface, ContainerFactoryPluginInterface {
/**
* The entity type bundle info service.
*
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
*/
protected $entityTypeBundleInfo;
/**
* @var \Drupal\Core\Entity\EntityTypeInterface|null
*/
protected $bundleOf;
/**
* Creates a new EntityBundle instance.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
* The entity type bundle info service.
* @param array $configuration
* The plugin configuration, i.e. an array with configuration values keyed
* by configuration option name. The special key 'context' may be used to
* initialize the defined contexts by setting it to an array of context
* values keyed by context names.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $entity_type_bundle_info, array $configuration, $plugin_id, $plugin_definition) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->entityTypeBundleInfo = $entity_type_bundle_info;
$this->bundleOf = $entity_type_manager->getDefinition($this->getDerivativeId());
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$container->get('entity_type.manager'),
$container->get('entity_type.bundle.info'),
$configuration,
$plugin_id,
$plugin_definition
);
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$options = array();
$bundles = $this->entityTypeBundleInfo->getBundleInfo($this->bundleOf->id());
foreach ($bundles as $id => $info) {
$options[$id] = $info['label'];
}
$form['bundles'] = array(
'#title' => $this->pluginDefinition['label'],
'#type' => 'checkboxes',
'#options' => $options,
'#default_value' => $this->configuration['bundles'],
);
return parent::buildConfigurationForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
$this->configuration['bundles'] = array_filter($form_state->getValue('bundles'));
parent::submitConfigurationForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function evaluate() {
if (empty($this->configuration['bundles']) && !$this->isNegated()) {
return TRUE;
}
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
$entity = $this->getContextValue($this->bundleOf->id());
return !empty($this->configuration['bundles'][$entity->bundle()]);
}
/**
* {@inheritdoc}
*/
public function summary() {
if (count($this->configuration['bundles']) > 1) {
$bundles = $this->configuration['bundles'];
$last = array_pop($bundles);
$bundles = implode(', ', $bundles);
return $this->t('@bundle_type is @bundles or @last', array('@bundle_type' => $this->bundleOf->getBundleLabel(), '@bundles' => $bundles, '@last' => $last));
}
$bundle = reset($this->configuration['bundles']);
return $this->t('@bundle_type is @bundle', array('@bundle_type' => $this->bundleOf->getBundleLabel(), '@bundle' => $bundle));
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return array('bundles' => array()) + parent::defaultConfiguration();
}
/**
* {@inheritdoc}
*
* @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts
*/
public function applyConstraints(array $contexts = array()) {
// Nullify any bundle constraints on contexts we care about.
$this->removeConstraints($contexts);
$bundle = array_values($this->configuration['bundles']);
// There's only one expected context for this plugint type.
foreach ($this->getContextMapping() as $definition_id => $context_id) {
$contexts[$context_id]->getContextDefinition()->addConstraint('Bundle', ['value' => $bundle]);
}
}
/**
* {@inheritdoc}
*
* @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts
*/
public function removeConstraints(array $contexts = array()) {
// Reset the bundle constraint for any context we've mapped.
foreach ($this->getContextMapping() as $definition_id => $context_id) {
$constraints = $contexts[$context_id]->getContextDefinition()->getConstraints();
unset($constraints['Bundle']);
$contexts[$context_id]->getContextDefinition()->setConstraints($constraints);
}
}
}

View file

@ -0,0 +1,41 @@
<?php
namespace Drupal\ctools\Plugin\Condition;
use Drupal\node\Plugin\Condition\NodeType as CoreNodeType;
use Drupal\ctools\ConstraintConditionInterface;
class NodeType extends CoreNodeType implements ConstraintConditionInterface {
/**
* {@inheritdoc}
*
* @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts
*/
public function applyConstraints(array $contexts = array()) {
// Nullify any bundle constraints on contexts we care about.
$this->removeConstraints($contexts);
// If a single bundle is configured, we can set a proper constraint.
if (count($this->configuration['bundles']) == 1) {
$bundle = array_values($this->configuration['bundles']);
foreach ($this->getContextMapping() as $definition_id => $context_id) {
$contexts[$context_id]->getContextDefinition()->addConstraint('Bundle', ['value' => $bundle[0]]);
}
}
}
/**
* {@inheritdoc}
*
* @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts
*/
public function removeConstraints(array $contexts = array()) {
// Reset the bundle constraint for any context we've mapped.
foreach ($this->getContextMapping() as $definition_id => $context_id) {
$constraints = $contexts[$context_id]->getContextDefinition()->getConstraints();
unset($constraints['Bundle']);
$contexts[$context_id]->getContextDefinition()->setConstraints($constraints);
}
}
}

View file

@ -0,0 +1,53 @@
<?php
namespace Drupal\ctools\Plugin\Deriver;
use Drupal\Core\Plugin\Context\ContextDefinition;
/**
* Deriver that creates a condition for each entity type with bundles.
*/
class EntityBundle extends EntityDeriverBase {
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
foreach ($this->entityManager->getDefinitions() as $entity_type_id => $entity_type) {
if ($entity_type->hasKey('bundle')) {
$this->derivatives[$entity_type_id] = $base_plugin_definition;
$this->derivatives[$entity_type_id]['label'] = $this->getEntityBundleLabel($entity_type);
$this->derivatives[$entity_type_id]['context'] = [
"$entity_type_id" => new ContextDefinition('entity:' . $entity_type_id),
];
}
}
return $this->derivatives;
}
/**
* Provides the bundle label with a fallback when not defined.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type we are looking the bundle label for.
*
* @return \Drupal\Core\StringTranslation\TranslatableMarkup
* The entity bundle label or a fallback label.
*/
protected function getEntityBundleLabel($entity_type) {
if ($label = $entity_type->getBundleLabel()) {
return $this->t('@label', ['@label' => $label]);
}
$fallback = $entity_type->getLabel();
if ($bundle_entity_type = $entity_type->getBundleEntityType()) {
// This is a better fallback.
$fallback = $this->entityManager->getDefinition($bundle_entity_type)->getLabel();
}
return $this->t('@label bundle', ['@label' => $fallback]);
}
}

View file

@ -0,0 +1,51 @@
<?php
namespace Drupal\ctools\Plugin\Deriver;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* An abstract base class that sets up the needs of entity specific derivers.
*/
abstract class EntityDeriverBase extends DeriverBase implements ContainerDeriverInterface {
use StringTranslationTrait;
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* Constructs new EntityViewDeriver.
*
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The string translation service.
*/
public function __construct(EntityManagerInterface $entity_manager, TranslationInterface $string_translation) {
$this->entityManager = $entity_manager;
$this->stringTranslation = $string_translation;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static(
$container->get('entity.manager'),
$container->get('string_translation')
);
}
}

View file

@ -0,0 +1,28 @@
<?php
namespace Drupal\ctools\Plugin\Deriver;
use Drupal\Core\Plugin\Context\ContextDefinition;
/**
* Provides entity view block definitions for each entity type.
*/
class EntityViewDeriver extends EntityDeriverBase {
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
foreach ($this->entityManager->getDefinitions() as $entity_type_id => $entity_type) {
if ($entity_type->hasViewBuilderClass()) {
$this->derivatives[$entity_type_id] = $base_plugin_definition;
$this->derivatives[$entity_type_id]['admin_label'] = $this->t('Entity view (@label)', ['@label' => $entity_type->getLabel()]);
$this->derivatives[$entity_type_id]['context'] = [
'entity' => new ContextDefinition('entity:' . $entity_type_id),
];
}
}
return $this->derivatives;
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace Drupal\ctools\Plugin\Deriver;
use Drupal\Core\TypedData\DataDefinitionInterface;
class TypedDataEntityRelationshipDeriver extends TypedDataRelationshipDeriver {
/**
* {@inheritdoc}
*/
protected $label = '@property Entity from @base';
/**
* {@inheritdoc}
*/
protected function generateDerivativeDefinition($base_plugin_definition, $data_type_id, $data_type_definition, DataDefinitionInterface $base_definition, $property_name, DataDefinitionInterface $property_definition) {
if (method_exists($property_definition, 'getType') && $property_definition->getType() == 'entity_reference') {
parent::generateDerivativeDefinition($base_plugin_definition, $data_type_id, $data_type_definition, $base_definition, $property_name, $property_definition);
}
}
}

View file

@ -0,0 +1,40 @@
<?php
namespace Drupal\ctools\Plugin\Deriver;
use Drupal\Core\TypedData\DataDefinitionInterface;
class TypedDataLanguageRelationshipDeriver extends TypedDataRelationshipDeriver {
/**
* {@inheritdoc}
*
* @todo this results in awful labels like "Language Language from Content"
* Fix it.
*/
protected $label = '@property Language from @base';
/**
* {@inheritdoc}
*/
protected function generateDerivativeDefinition($base_plugin_definition, $data_type_id, $data_type_definition, DataDefinitionInterface $base_definition, $property_name, DataDefinitionInterface $property_definition) {
if (method_exists($property_definition, 'getType') && $property_definition->getType() == 'language') {
parent::generateDerivativeDefinition($base_plugin_definition, $data_type_id, $data_type_definition, $base_definition, $property_name, $property_definition);
}
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
parent::getDerivativeDefinitions($base_plugin_definition);
// The data types will all be set to string since language extends string
// and the parent class finds the related primitive.
foreach ($this->derivatives as $plugin_id => $derivative) {
$this->derivatives[$plugin_id]['data_type'] = 'language';
}
return $this->derivatives;
}
}

View file

@ -0,0 +1,113 @@
<?php
namespace Drupal\ctools\Plugin\Deriver;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\Core\TypedData\ComplexDataInterface;
use Drupal\Core\TypedData\DataDefinitionInterface;
use Drupal\Core\TypedData\DataReferenceDefinitionInterface;
use Drupal\Core\TypedData\ListDataDefinitionInterface;
use Drupal\Core\TypedData\TypedDataManagerInterface;
use Drupal\field\Entity\FieldConfig;
use Symfony\Component\DependencyInjection\ContainerInterface;
abstract class TypedDataPropertyDeriverBase extends DeriverBase implements ContainerDeriverInterface {
use StringTranslationTrait;
/**
* @var \Drupal\Core\TypedData\TypedDataManagerInterface
*/
protected $typedDataManager;
/**
* The label string for use with derivative labels.
*
* @var string
*/
protected $label = '@property from @base';
/**
* TypedDataPropertyDeriverBase constructor.
*
* @param \Drupal\Core\TypedData\TypedDataManagerInterface $typed_data_manager
* The typed data manager.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The string translation service.
*/
public function __construct(TypedDataManagerInterface $typed_data_manager, TranslationInterface $string_translation) {
$this->typedDataManager = $typed_data_manager;
$this->stringTranslation = $string_translation;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static(
$container->get('typed_data_manager'),
$container->get('string_translation')
);
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
foreach ($this->typedDataManager->getDefinitions() as $data_type_id => $data_type_definition) {
if (is_subclass_of($data_type_definition['class'], ComplexDataInterface::class, TRUE)) {
/** @var \Drupal\Core\TypedData\ComplexDataDefinitionInterface $base_definition */
$base_definition = $this->typedDataManager->createDataDefinition($data_type_id);
foreach ($base_definition->getPropertyDefinitions() as $property_name => $property_definition) {
if ($property_definition instanceof BaseFieldDefinition || $property_definition instanceof FieldConfig) {
$this->generateDerivativeDefinition($base_plugin_definition, $data_type_id, $data_type_definition, $base_definition, $property_name, $property_definition);
}
}
}
}
return $this->derivatives;
}
/**
* @param $property_definition
*
* @return mixed
*/
protected function getDataType($property_definition) {
if ($property_definition instanceof DataReferenceDefinitionInterface) {
return $property_definition->getTargetDefinition()->getDataType();
}
if ($property_definition instanceof ListDataDefinitionInterface) {
return $property_definition->getItemDefinition()->getDataType();
}
return $property_definition->getDataType();
}
/**
* Generates and maintains a derivative definition.
*
* This method should directly manipulate $this->derivatives and not return
* values. This allows implementations control over the derivative names.
*
* @param $base_plugin_definition
* The base plugin definition.
* @param string $data_type_id
* The plugin id of the data type.
* @param mixed $data_type_definition
* The plugin definition of the data type.
* @param \Drupal\Core\TypedData\DataDefinitionInterface $base_definition
* The data type definition of a complex data object.
* @param string $property_name
* The name of the property
* @param \Drupal\Core\TypedData\DataDefinitionInterface $property_definition
* The property definition.
*
*/
abstract protected function generateDerivativeDefinition($base_plugin_definition, $data_type_id, $data_type_definition, DataDefinitionInterface $base_definition, $property_name, DataDefinitionInterface $property_definition);
}

View file

@ -0,0 +1,75 @@
<?php
namespace Drupal\ctools\Plugin\Deriver;
use Drupal\Core\Plugin\Context\ContextDefinition;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\Core\TypedData\DataDefinitionInterface;
use Drupal\field\FieldConfigInterface;
class TypedDataRelationshipDeriver extends TypedDataPropertyDeriverBase implements ContainerDeriverInterface {
/**
* {@inheritdoc}
*/
protected function generateDerivativeDefinition($base_plugin_definition, $data_type_id, $data_type_definition, DataDefinitionInterface $base_definition, $property_name, DataDefinitionInterface $property_definition) {
$bundle_info = $base_definition->getConstraint('Bundle');
// Identify base definitions that appear on bundle-able entities.
if ($bundle_info && array_filter($bundle_info) && $base_definition->getConstraint('EntityType')) {
$base_data_type = 'entity:' . $base_definition->getConstraint('EntityType');
}
// Otherwise, just use the raw data type identifier.
else {
$base_data_type = $data_type_id;
}
// If we've not processed this thing before.
if (!isset($this->derivatives[$base_data_type . ':' . $property_name])) {
$derivative = $base_plugin_definition;
$derivative['label'] = $this->t($this->label, [
'@property' => $property_definition->getLabel(),
'@base' => $data_type_definition['label'],
]);
$derivative['data_type'] = $property_definition->getFieldStorageDefinition()->getPropertyDefinition($property_definition->getFieldStorageDefinition()->getMainPropertyName())->getDataType();
$derivative['property_name'] = $property_name;
$context_definition = new ContextDefinition($base_data_type, $this->typedDataManager->createDataDefinition($base_data_type));
// Add the constraints of the base definition to the context definition.
if ($base_definition->getConstraint('Bundle')) {
$context_definition->addConstraint('Bundle', $base_definition->getConstraint('Bundle'));
}
$derivative['context'] = [
'base' => $context_definition,
];
$derivative['property_name'] = $property_name;
$this->derivatives[$base_data_type . ':' . $property_name] = $derivative;
}
// Individual fields can be on multiple bundles.
elseif ($property_definition instanceof FieldConfigInterface) {
// We should only end up in here on entity bundles.
$derivative = $this->derivatives[$base_data_type . ':' . $property_name];
// Update label
/** @var \Drupal\Core\StringTranslation\TranslatableMarkup $label */
$label = $derivative['label'];
list(,, $argument_name) = explode(':', $data_type_id);
$arguments = $label->getArguments();
$arguments['@'. $argument_name] = $data_type_definition['label'];
$string_args = $arguments;
array_shift($string_args);
$last = array_slice($string_args, -1);
// The slice doesn't remove, so do that now.
array_pop($string_args);
$string = count($string_args) >= 2 ? '@property from '. implode(', ', array_keys($string_args)) .' and '. array_keys($last)[0] : '@property from @base and '. array_keys($last)[0];
$this->derivatives[$base_data_type . ':' . $property_name]['label'] = $this->t($string, $arguments);
if ($base_definition->getConstraint('Bundle')) {
// Add bundle constraints
$context_definition = $derivative['context']['base'];
$bundles = $context_definition->getConstraint('Bundle') ?: [];
$bundles = array_merge($bundles, $base_definition->getConstraint('Bundle'));
$context_definition->addConstraint('Bundle', $bundles);
}
}
}
}

View file

@ -0,0 +1,226 @@
<?php
namespace Drupal\ctools\Plugin\DisplayVariant;
use Drupal\Component\Uuid\UuidInterface;
use Drupal\Core\Block\BlockManager;
use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
use Drupal\Core\Condition\ConditionManager;
use Drupal\Core\Display\VariantBase;
use Drupal\Core\Display\ContextAwareVariantInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\Context\ContextHandlerInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Utility\Token;
use Drupal\ctools\Form\AjaxFormTrait;
use Drupal\ctools\Plugin\BlockVariantInterface;
use Drupal\ctools\Plugin\BlockVariantTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a base class for a display variant that simply contains blocks.
*/
abstract class BlockDisplayVariant extends VariantBase implements ContextAwareVariantInterface, ContainerFactoryPluginInterface, BlockVariantInterface, RefinableCacheableDependencyInterface {
use AjaxFormTrait;
use BlockVariantTrait;
/**
* The context handler.
*
* @var \Drupal\Core\Plugin\Context\ContextHandlerInterface
*/
protected $contextHandler;
/**
* The UUID generator.
*
* @var \Drupal\Component\Uuid\UuidInterface
*/
protected $uuidGenerator;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $account;
/**
* The token service.
*
* @var \Drupal\Core\Utility\Token
*/
protected $token;
/**
* An array of collected contexts.
*
* This is only used on runtime, and is not stored.
*
* @var \Drupal\Component\Plugin\Context\ContextInterface[]
*/
protected $contexts = [];
/**
* Constructs a new BlockDisplayVariant.
*
* @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\Plugin\Context\ContextHandlerInterface $context_handler
* The context handler.
* @param \Drupal\Core\Session\AccountInterface $account
* The current user.
* @param \Drupal\Component\Uuid\UuidInterface $uuid_generator
* The UUID generator.
* @param \Drupal\Core\Utility\Token $token
* The token service.
* @param \Drupal\Core\Block\BlockManager $block_manager
* The block manager.
* @param \Drupal\Core\Condition\ConditionManager $condition_manager
* The condition manager.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, ContextHandlerInterface $context_handler, AccountInterface $account, UuidInterface $uuid_generator, Token $token, BlockManager $block_manager, ConditionManager $condition_manager) {
// Inject dependencies as early as possible, so they can be used in
// configuration.
$this->contextHandler = $context_handler;
$this->account = $account;
$this->uuidGenerator = $uuid_generator;
$this->token = $token;
$this->blockManager = $block_manager;
$this->conditionManager = $condition_manager;
parent::__construct($configuration, $plugin_id, $plugin_definition);
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('context.handler'),
$container->get('current_user'),
$container->get('uuid'),
$container->get('token'),
$container->get('plugin.manager.block'),
$container->get('plugin.manager.condition')
);
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return parent::defaultConfiguration() + [
'blocks' => []
];
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
foreach ($this->getBlockCollection() as $instance) {
$this->calculatePluginDependencies($instance);
}
return $this->dependencies;
}
/**
* {@inheritdoc}
*/
public function getConfiguration() {
return [
'blocks' => $this->getBlockCollection()->getConfiguration(),
] + parent::getConfiguration();
}
/**
* {@inheritdoc}
*/
public function setConfiguration(array $configuration) {
// preserve the uuid.
if ($this->configuration && !empty($this->configuration['uuid'])) {
$configuration['uuid'] = $this->configuration['uuid'];
}
parent::setConfiguration($configuration);
$this->getBlockCollection()->setConfiguration($this->configuration['blocks']);
return $this;
}
/**
* Gets the contexts.
*
* @return \Drupal\Component\Plugin\Context\ContextInterface[]
* An array of set contexts, keyed by context name.
*/
public function getContexts() {
return $this->contexts;
}
/**
* Sets the contexts.
*
* @param \Drupal\Component\Plugin\Context\ContextInterface[] $contexts
* An array of contexts, keyed by context name.
*
* @return $this
*/
public function setContexts(array $contexts) {
$this->contexts = $contexts;
return $this;
}
/**
* {@inheritdoc}
*/
protected function contextHandler() {
return $this->contextHandler;
}
/**
* {@inheritdoc}
*/
protected function getBlockConfig() {
return $this->configuration['blocks'];
}
/**
* {@inheritdoc}
*/
protected function uuidGenerator() {
return $this->uuidGenerator;
}
/**
* {@inheritdoc}
*/
public function __sleep() {
$vars = parent::__sleep();
// Gathered contexts objects should not be serialized.
if (($key = array_search('contexts', $vars)) !== FALSE) {
unset($vars[$key]);
}
// The block plugin collection should also not be serialized, ensure that
// configuration is synced back.
if (($key = array_search('blockPluginCollection', $vars)) !== FALSE) {
if ($this->blockPluginCollection) {
$this->configuration['blocks'] = $this->blockPluginCollection->getConfiguration();
}
unset($vars[$key]);
}
return $vars;
}
}

View file

@ -0,0 +1,25 @@
<?php
namespace Drupal\ctools\Plugin;
/**
* Provides an interface for configuring a plugin via wizard steps.
*/
interface PluginWizardInterface {
/**
* Retrieve a list of FormInterface classes by their step key in the wizard.
*
* @param mixed $cached_values
* The cached values used in the wizard. The plugin we're editing will
* always be assigned to the 'plugin' key.
*
* @return array
* An associative array keyed on the step name with an array value with the
* following keys:
* - title (string): Human-readable title of the step.
* - form (string): Fully-qualified class name of the form for this step.
*/
public function getWizardOperations($cached_values);
}

View file

@ -0,0 +1,36 @@
<?php
namespace Drupal\ctools\Plugin\Relationship;
use Drupal\Core\Plugin\Context\Context;
use Drupal\Core\Plugin\Context\ContextDefinition;
/**
* @Relationship(
* id = "typed_data_entity_relationship",
* deriver = "\Drupal\ctools\Plugin\Deriver\TypedDataEntityRelationshipDeriver"
* )
*/
class TypedDataEntityRelationship extends TypedDataRelationship {
/**
* {@inheritdoc}
*/
public function getRelationship() {
$plugin_definition = $this->getPluginDefinition();
$entity_type = $this->getData($this->getContext('base'))->getDataDefinition()->getSetting('target_type');
$context_definition = new ContextDefinition("entity:$entity_type", $plugin_definition['label']);
$context_value = NULL;
// If the 'base' context has a value, then get the property value to put on
// the context (otherwise, mapping hasn't occurred yet and we just want to
// return the context with the right definition and no value).
if ($this->getContext('base')->hasContextValue()) {
$context_value = $this->getData($this->getContext('base'))->entity;
}
$context_definition->setDefaultValue($context_value);
return new Context($context_definition, $context_value);
}
}

View file

@ -0,0 +1,35 @@
<?php
namespace Drupal\ctools\Plugin\Relationship;
use Drupal\Core\Plugin\Context\Context;
use Drupal\Core\Plugin\Context\ContextDefinition;
/**
* @Relationship(
* id = "typed_data_language_relationship",
* deriver = "\Drupal\ctools\Plugin\Deriver\TypedDataLanguageRelationshipDeriver"
* )
*/
class TypedDataLanguageRelationship extends TypedDataRelationship {
/**
* {@inheritdoc}
*/
public function getRelationship() {
$plugin_definition = $this->getPluginDefinition();
$context_definition = new ContextDefinition("language", $plugin_definition['label']);
$context_value = NULL;
// If the 'base' context has a value, then get the property value to put on
// the context (otherwise, mapping hasn't occurred yet and we just want to
// return the context with the right definition and no value).
if ($this->getContext('base')->hasContextValue()) {
$context_value = $this->getData($this->getContext('base'))->language;
}
$context_definition->setDefaultValue($context_value);
return new Context($context_definition, $context_value);
}
}

View file

@ -0,0 +1,77 @@
<?php
namespace Drupal\ctools\Plugin\Relationship;
use Drupal\Core\Field\FieldItemInterface;
use Drupal\Core\Field\TypedData\FieldItemDataDefinition;
use Drupal\Core\Plugin\Context\Context;
use Drupal\Core\Plugin\Context\ContextDefinition;
use Drupal\Core\Plugin\Context\ContextInterface;
use Drupal\Core\TypedData\DataReferenceInterface;
use Drupal\Core\TypedData\ListInterface;
use Drupal\ctools\Annotation\Relationship;
use Drupal\ctools\Plugin\RelationshipBase;
/**
* @Relationship(
* id = "typed_data_relationship",
* deriver = "\Drupal\ctools\Plugin\Deriver\TypedDataRelationshipDeriver"
* )
*/
class TypedDataRelationship extends RelationshipBase {
/**
* {@inheritdoc}
*/
public function getRelationship() {
$plugin_definition = $this->getPluginDefinition();
$data_type = $plugin_definition['data_type'];
$context_definition = new ContextDefinition($data_type, $plugin_definition['label']);
$context_value = NULL;
// If the 'base' context has a value, then get the property value to put on
// the context (otherwise, mapping hasn't occurred yet and we just want to
// return the context with the right definition and no value).
if ($this->getContext('base')->hasContextValue()) {
$data = $this->getData($this->getContext('base'));
$property = $this->getMainPropertyName($data);
$context_value = $data->get($property)->getValue();
}
$context_definition->setDefaultValue($context_value);
return new Context($context_definition, $context_value);
}
public function getName() {
return $this->getPluginDefinition()['property_name'];
}
protected function getData(ContextInterface $context) {
/** @var \Drupal\Core\TypedData\ComplexDataInterface $base */
$base = $context->getContextValue();
$name = $this->getPluginDefinition()['property_name'];
$data = $base->get($name);
// @todo add configuration to get N instead of first.
if ($data instanceof ListInterface) {
$data = $data->first();
}
if ($data instanceof DataReferenceInterface) {
$data = $data->getTarget();
}
return $data;
}
protected function getMainPropertyName(FieldItemInterface $data) {
return $data->getFieldDefinition()->getFieldStorageDefinition()->getMainPropertyName();
}
public function getRelationshipValue() {
$property = $this->getMainPropertyName();
/** @var \Drupal\Core\TypedData\ComplexDataInterface $data */
$data = $this->getRelationship()->getContextData();
$data->get($property)->getValue();
}
}

View file

@ -0,0 +1,10 @@
<?php
namespace Drupal\ctools\Plugin;
use Drupal\Core\Plugin\ContextAwarePluginBase;
/**
* Base class for Relationship plugins.
*/
abstract class RelationshipBase extends ContextAwarePluginBase implements RelationshipInterface {}

View file

@ -0,0 +1,27 @@
<?php
namespace Drupal\ctools\Plugin;
use Drupal\Component\Plugin\DerivativeInspectionInterface;
use Drupal\Core\Plugin\ContextAwarePluginInterface;
/**
* Defines an interface for Relationship plugins.
*/
interface RelationshipInterface extends ContextAwarePluginInterface, DerivativeInspectionInterface {
/**
* Generates a context based on this plugin's configuration.
*
* @return \Drupal\Core\Plugin\Context\ContextInterface
*/
public function getRelationship();
/**
* The name of the property used to get this relationship.
*
* @return string
*/
public function getName();
}

View file

@ -0,0 +1,35 @@
<?php
namespace Drupal\ctools\Plugin;
use Drupal\Core\Plugin\Context\ContextAwarePluginManagerTrait;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
/**
* Provides the Relationship plugin manager.
*/
class RelationshipManager extends DefaultPluginManager implements RelationshipManagerInterface {
use ContextAwarePluginManagerTrait;
/**
* Constructor for RelationshipManager objects.
*
* @param \Traversable $namespaces
* An object that implements \Traversable which contains the root paths
* keyed by the corresponding namespace to look for plugin implementations.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* Cache backend instance to use.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler to invoke the alter hook with.
*/
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
parent::__construct('Plugin/Relationship', $namespaces, $module_handler, 'Drupal\ctools\Plugin\RelationshipInterface', 'Drupal\ctools\Annotation\Relationship');
$this->alterInfo('ctools_relationship_info');
$this->setCacheBackend($cache_backend, 'ctools_relationship_plugins');
}
}

View file

@ -0,0 +1,10 @@
<?php
namespace Drupal\ctools\Plugin;
use Drupal\Component\Plugin\Discovery\CachedDiscoveryInterface;
use Drupal\Core\Plugin\Context\ContextAwarePluginManagerInterface;
/**
* Provides the Relationship plugin manager.
*/
interface RelationshipManagerInterface extends ContextAwarePluginManagerInterface, CachedDiscoveryInterface {}

View file

@ -0,0 +1,50 @@
<?php
namespace Drupal\ctools\Plugin;
/**
* Provides an interface for objects that have variants e.g. Pages.
*/
interface VariantCollectionInterface {
/**
* Adds a new variant to the entity.
*
* @param array $configuration
* An array of configuration for the new variant.
*
* @return string
* The variant ID.
*/
public function addVariant(array $configuration);
/**
* Retrieves a specific variant.
*
* @param string $variant_id
* The variant ID.
*
* @return \Drupal\Core\Display\VariantInterface
* The variant object.
*/
public function getVariant($variant_id);
/**
* Removes a specific variant.
*
* @param string $variant_id
* The variant ID.
*
* @return $this
*/
public function removeVariant($variant_id);
/**
* Returns the variants available for the entity.
*
* @return \Drupal\Core\Display\VariantInterface[]
* An array of the variants.
*/
public function getVariants();
}

View file

@ -0,0 +1,67 @@
<?php
namespace Drupal\ctools\Plugin;
/**
* Provides methods for VariantCollectionInterface.
*/
trait VariantCollectionTrait {
/**
* The plugin collection that holds the variants.
*
* @var \Drupal\ctools\Plugin\VariantPluginCollection
*/
protected $variantCollection;
/**
* @see \Drupal\ctools\Plugin\VariantCollectionInterface::addVariant()
*/
public function addVariant(array $configuration) {
$configuration['uuid'] = $this->uuidGenerator()->generate();
$this->getVariants()->addInstanceId($configuration['uuid'], $configuration);
return $configuration['uuid'];
}
/**
* @see \Drupal\ctools\Plugin\VariantCollectionInterface::getVariant()
*/
public function getVariant($variant_id) {
return $this->getVariants()->get($variant_id);
}
/**
* @see \Drupal\ctools\Plugin\VariantCollectionInterface::removeVariant()
*/
public function removeVariant($variant_id) {
$this->getVariants()->removeInstanceId($variant_id);
return $this;
}
/**
* @see \Drupal\ctools\Plugin\VariantCollectionInterface::getVariants()
*/
public function getVariants() {
if (!$this->variantCollection) {
$this->variantCollection = new VariantPluginCollection(\Drupal::service('plugin.manager.display_variant'), $this->getVariantConfig());
$this->variantCollection->sort();
}
return $this->variantCollection;
}
/**
* Returns the configuration for stored variants.
*
* @return array
* An array of variant configuration, keyed by the unique variant ID.
*/
abstract protected function getVariantConfig();
/**
* Returns the UUID generator.
*
* @return \Drupal\Component\Uuid\UuidInterface
*/
abstract protected function uuidGenerator();
}

View file

@ -0,0 +1,43 @@
<?php
namespace Drupal\ctools\Plugin;
use Drupal\Core\Plugin\DefaultLazyPluginCollection;
/**
* Provides a collection of variants plugins.
*/
class VariantPluginCollection extends DefaultLazyPluginCollection {
/**
* {@inheritdoc}
*
* @return \Drupal\Core\Display\VariantInterface
*/
public function &get($instance_id) {
return parent::get($instance_id);
}
/**
* {@inheritdoc}
*/
public function sort() {
// @todo Determine the reason this needs error suppression.
@uasort($this->instanceIDs, [$this, 'sortHelper']);
return $this;
}
/**
* {@inheritdoc}
*/
public function sortHelper($aID, $bID) {
$a_weight = $this->get($aID)->getWeight();
$b_weight = $this->get($bID)->getWeight();
if ($a_weight == $b_weight) {
return strcmp($aID, $bID);
}
return ($a_weight < $b_weight) ? -1 : 1;
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace Drupal\ctools\Routing\Enhancer;
use Drupal\Core\Routing\Enhancer\RouteEnhancerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Route;
/**
* Sets the request format onto the request object.
*/
class WizardEnhancer implements RouteEnhancerInterface {
/**
* {@inheritdoc}
*/
public function applies(Route $route) {
return !$route->hasDefault('_controller') && ($route->hasDefault('_wizard') || $route->hasDefault('_entity_wizard'));
}
/**
* {@inheritdoc}
*/
public function enhance(array $defaults, Request $request) {
if (!empty($defaults['_wizard'])) {
$defaults['_controller'] = 'ctools.wizard.form:getContentResult';
}
if (!empty($defaults['_entity_wizard'])) {
$defaults['_controller'] = 'ctools.wizard.entity.form:getContentResult';
}
return $defaults;
}
}

View file

@ -0,0 +1,13 @@
<?php
namespace Drupal\ctools;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\user\SharedTempStore;
/**
* An extension of the SharedTempStore system for serialized data.
*/
class SerializableTempstore extends SharedTempStore {
use DependencySerializationTrait;
}

View file

@ -0,0 +1,27 @@
<?php
namespace Drupal\ctools;
use Drupal\user\SharedTempStoreFactory;
/**
* A factory for creating SerializableTempStore objects.
*/
class SerializableTempstoreFactory extends SharedTempStoreFactory {
/**
* {@inheritdoc}
*/
function get($collection, $owner = NULL) {
// Use the currently authenticated user ID or the active user ID unless the
// owner is overridden.
if (!isset($owner)) {
$owner = \Drupal::currentUser()->id() ?: session_id();
}
// Store the data for this collection in the database.
$storage = $this->storageFactory->get("user.shared_tempstore.$collection");
return new SerializableTempstore($storage, $this->lockBackend, $owner, $this->requestStack, $this->expire);
}
}

View file

@ -0,0 +1,55 @@
<?php
namespace Drupal\ctools\Testing;
use Drupal\Component\Render\FormattableMarkup;
trait EntityCreationTrait {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Creates a custom content type based on default settings.
*
* @param string $entity_type
* The type of entity to create.
* @param array $values
* An array of settings to change from the defaults.
* Example: 'type' => 'foo'.
*
* @return \Drupal\Core\Entity\EntityInterface
* Created entity.
*/
protected function createEntity($entity_type, array $values = array()) {
$storage = $this->getEntityTypeManager()->getStorage($entity_type);
$entity = $storage->create($values);
$status = $entity->save();
\Drupal::service('router.builder')->rebuild();
if ($this instanceof \PHPUnit_Framework_TestCase) {
$this->assertSame($status, SAVED_NEW, (new FormattableMarkup('Created entity %id of type %type.', ['%id' => $entity->id(), '%type' => $entity_type]))->__toString());
}
else {
$this->assertEqual($status, SAVED_NEW, (new FormattableMarkup('Created entity %id of type %type.', ['%id' => $entity->id(), '%type' => $entity_type]))->__toString());
}
return $entity;
}
/**
* @return \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected function getEntityTypeManager() {
if (!isset($this->entityTypeManager)) {
$this->entityTypeManager = $this->container->get('entity_type.manager');
}
return $this->entityTypeManager;
}
}

View file

@ -0,0 +1,136 @@
<?php
namespace Drupal\ctools\Tests\Wizard;
use Drupal\simpletest\WebTestBase;
use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* Tests basic wizard functionality.
*
* @group ctools
*/
class CToolsWizardTest extends WebTestBase {
use StringTranslationTrait;
public static $modules = array('ctools', 'ctools_wizard_test');
function testWizardSteps() {
$this->drupalGet('ctools/wizard');
$this->assertText('Form One');
$this->dumpHeaders = TRUE;
// Check that $operations['one']['values'] worked.
$this->assertText('Xylophone');
// Submit first step in the wizard.
$edit = [
'one' => 'test',
];
$this->drupalPostForm('ctools/wizard', $edit, $this->t('Next'));
// Redirected to the second step.
$this->assertText('Form Two');
$this->assertText('Dynamic value submitted: Xylophone');
// Check that $operations['two']['values'] worked.
$this->assertText('Zebra');
// Hit previous to make sure our form value are preserved.
$this->drupalPostForm(NULL, [], $this->t('Previous'));
// Check the known form values.
$this->assertFieldByName('one', 'test');
$this->assertText('Xylophone');
// Goto next step again and finish this wizard.
$this->drupalPostForm(NULL, [], $this->t('Next'));
$edit = [
'two' => 'Second test',
];
$this->drupalPostForm(NULL, $edit, $this->t('Finish'));
// Check that the wizard finished properly.
$this->assertText('Value One: test');
$this->assertText('Value Two: Second test');
}
function testStepValidateAndSubmit() {
$this->drupalGet('ctools/wizard');
$this->assertText('Form One');
// Submit first step in the wizard.
$edit = [
'one' => 'wrong',
];
$this->drupalPostForm('ctools/wizard', $edit, $this->t('Next'));
// We're still on the first form and the error is present.
$this->assertText('Form One');
$this->assertText('Cannot set the value to "wrong".');
// Try again with the magic value.
$edit = [
'one' => 'magic',
];
$this->drupalPostForm('ctools/wizard', $edit, $this->t('Next'));
// Redirected to the second step.
$this->assertText('Form Two');
$edit = [
'two' => 'Second test',
];
$this->drupalPostForm(NULL, $edit, $this->t('Finish'));
// Check that the magic value triggered our submit callback.
$this->assertText('Value One: Abraham');
$this->assertText('Value Two: Second test');
}
function testEntityWizard() {
$this->drupalLogin($this->drupalCreateUser(['administer site configuration']));
// Start adding a new config entity.
$this->drupalGet('admin/structure/ctools_wizard_test_config_entity/add');
$this->assertText('Example entity');
$this->assertNoText('Existing entity');
// Submit the general step.
$edit = [
'id' => 'test123',
'label' => 'Test Config Entity 123',
];
$this->drupalPostForm(NULL, $edit, $this->t('Next'));
// Submit the first step.
$edit = [
'one' => 'The first bit',
];
$this->drupalPostForm(NULL, $edit, $this->t('Next'));
// Submit the second step.
$edit = [
'two' => 'The second bit',
];
$this->drupalPostForm(NULL, $edit, $this->t('Finish'));
// Now we should be looking at the list of entities.
$this->assertUrl('admin/structure/ctools_wizard_test_config_entity');
$this->assertText('Test Config Entity 123');
// Edit the entity again and make sure the values are what we expect.
$this->clickLink(t('Edit'));
$this->assertText('Existing entity');
$this->assertFieldByName('label', 'Test Config Entity 123');
$this->clickLink(t('Form One'));
$this->assertFieldByName('one', 'The first bit');
$previous = $this->getUrl();
$this->clickLink(t('Show on dialog'));
$this->assertRaw('Value from one: The first bit');
$this->drupalGet($previous);
// Change the value for 'one'.
$this->drupalPostForm(NULL, ['one' => 'New value'], $this->t('Next'));
$this->assertFieldByName('two', 'The second bit');
$this->drupalPostForm(NULL, [], $this->t('Next'));
// Make sure we get the additional step because the entity exists.
$this->assertText('This step only shows if the entity is already existing!');
$this->drupalPostForm(NULL, [], $this->t('Finish'));
// Edit the entity again and make sure the change stuck.
$this->assertUrl('admin/structure/ctools_wizard_test_config_entity');
$this->clickLink(t('Edit'));
$this->drupalPostForm(NULL, [], $this->t('Next'));
$this->assertFieldByName('one', 'New value');
}
}

View file

@ -0,0 +1,253 @@
<?php
namespace Drupal\ctools;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Plugin\Context\Context;
use Drupal\Core\Plugin\Context\ContextDefinition;
use Drupal\Core\Plugin\Context\ContextDefinitionInterface;
use Drupal\Core\Plugin\Context\ContextInterface;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\Core\TypedData\ComplexDataDefinitionInterface;
use Drupal\Core\TypedData\DataReferenceDefinitionInterface;
use Drupal\Core\TypedData\DataReferenceInterface;
use Drupal\Core\TypedData\ListDataDefinitionInterface;
use Drupal\Core\TypedData\ListInterface;
use Drupal\Core\TypedData\TypedDataManagerInterface;
class TypedDataResolver {
/**
* The typed data manager.
*
* @var \Drupal\Core\TypedData\TypedDataManagerInterface
*/
protected $manager;
/**
* The string translation service.
*
* @var \Drupal\Core\StringTranslation\TranslationInterface
*/
protected $translation;
/**
* @param \Drupal\Core\TypedData\TypedDataManagerInterface $manager
* The typed data manager.
* @param \Drupal\Core\StringTranslation\TranslationInterface $translation
* The string translation service.
*/
public function __construct(TypedDataManagerInterface $manager, TranslationInterface $translation) {
$this->manager = $manager;
$this->translation = $translation;
}
/**
* Convert a property to a context.
*
* This method will respect the value of contexts as well, so if a context
* object is pass that contains a value, the appropriate value will be
* extracted and injected into the resulting context object if available.
*
* @param string $property_path
* The name of the property.
* @param \Drupal\Core\Plugin\Context\ContextInterface $context
* The context from which we will extract values if available.
*
* @return \Drupal\Core\Plugin\Context\Context
* A context object that represents the definition & value of the property.
* @throws \Exception
*/
public function getContextFromProperty($property_path, ContextInterface $context) {
$value = NULL;
$data_definition = NULL;
if ($context->hasContextValue()) {
/** @var \Drupal\Core\TypedData\ComplexDataInterface $data */
$data = $context->getContextData();
foreach (explode(':', $property_path) as $name) {
if ($data instanceof ListInterface) {
if (!is_numeric($name)) {
// Implicitly default to delta 0 for lists when not specified.
$data = $data->first();
}
else {
// If we have a delta, fetch it and continue with the next part.
$data = $data->get($name);
continue;
}
}
// Forward to the target value if this is a data reference.
if ($data instanceof DataReferenceInterface) {
$data = $data->getTarget();
}
if (!$data->getDataDefinition()->getPropertyDefinition($name)) {
throw new \Exception("Unknown property $name in property path $property_path");
}
$data = $data->get($name);
}
$value = $data->getValue();
$data_definition = $data instanceof DataReferenceInterface ? $data->getDataDefinition()->getTargetDefinition() : $data->getDataDefinition();
}
else {
/** @var \Drupal\Core\TypedData\ComplexDataDefinitionInterface $data_definition */
$data_definition = $context->getContextDefinition()->getDataDefinition();
foreach (explode(':', $property_path) as $name) {
if ($data_definition instanceof ListDataDefinitionInterface) {
$data_definition = $data_definition->getItemDefinition();
// If the delta was specified explicitly, continue with the next part.
if (is_numeric($name)) {
continue;
}
}
// Forward to the target definition if this is a data reference
// definition.
if ($data_definition instanceof DataReferenceDefinitionInterface) {
$data_definition = $data_definition->getTargetDefinition();
}
if (!$data_definition->getPropertyDefinition($name)) {
throw new \Exception("Unknown property $name in property path $property_path");
}
$data_definition = $data_definition->getPropertyDefinition($name);
}
// Forward to the target definition if this is a data reference
// definition.
if ($data_definition instanceof DataReferenceDefinitionInterface) {
$data_definition = $data_definition->getTargetDefinition();
}
}
$context_definition = new ContextDefinition($data_definition->getDataType(), $data_definition->getLabel(), $data_definition->isRequired(), FALSE, $data_definition->getDescription());
return new Context($context_definition, $value);
}
/**
* Extracts a context from an array of contexts by a tokenized pattern.
*
* This is more than simple isset/empty checks on the contexts array. The
* pattern could be node:uid:name which will iterate over all provided
* contexts in the array for one named 'node', it will then load the data
* definition of 'node' and check for a property named 'uid'. This will then
* set a new (temporary) context on the array and recursively call itself to
* navigate through related properties all the way down until the request
* property is located. At that point the property is passed to a
* TypedDataResolver which will convert it to an appropriate ContextInterface
* object.
*
* @param $token
* A ":" delimited set of tokens representing
* @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts
* The array of available contexts.
*
* @return \Drupal\Core\Plugin\Context\ContextInterface
* The requested token as a full Context object.
*
* @throws \Drupal\ctools\ContextNotFoundException
*/
public function convertTokenToContext($token, $contexts) {
// If the requested token is already a context, just return it.
if (isset($contexts[$token])) {
return $contexts[$token];
}
else {
list($base, $property_path) = explode(':', $token, 2);
// A base must always be set. This method recursively calls itself
// setting bases for this reason.
if (!empty($contexts[$base])) {
return $this->getContextFromProperty($property_path, $contexts[$base]);
}
// @todo improve this exception message.
throw new ContextNotFoundException("The requested context was not found in the supplied array of contexts.");
}
}
/**
* Provides an administrative label for a tokenized relationship.
*
* @param string $token
* The token related to a context in the contexts array.
* @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts
* An array of contexts from which to extract our token's label.
*
* @return \Drupal\Core\StringTranslation\TranslatableMarkup
* The administrative label of $token.
*/
public function getLabelByToken($token, $contexts) {
// @todo Optimize this by allowing to limit the desired token?
$tokens = $this->getTokensForContexts($contexts);
if (isset($tokens[$token])) {
return $tokens[$token];
}
}
/**
* Extracts an array of tokens and labels.
*
* @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts
* The array of contexts with which we are currently dealing.
*
* @return array
* An array of token keys and corresponding labels.
*/
public function getTokensForContexts($contexts) {
$tokens = [];
foreach ($contexts as $context_id => $context) {
$data_definition = $context->getContextDefinition()->getDataDefinition();
if ($data_definition instanceof ComplexDataDefinitionInterface) {
foreach ($this->getTokensFromComplexData($data_definition) as $token => $label) {
$tokens["$context_id:$token"] = $data_definition->getLabel() . ': ' . $label;
}
}
}
return $tokens;
}
/**
* Returns tokens for a complex data definition.
*
* @param \Drupal\Core\TypedData\ComplexDataDefinitionInterface $complex_data_definition
*
* @return array
* An array of token keys and corresponding labels.
*/
protected function getTokensFromComplexData(ComplexDataDefinitionInterface $complex_data_definition) {
$tokens = [];
// Loop over all properties.
foreach ($complex_data_definition->getPropertyDefinitions() as $property_name => $property_definition) {
// Item definitions do not always have a label. Use the list definition
// label if the item does not have one.
$property_label = $property_definition->getLabel();
if ($property_definition instanceof ListDataDefinitionInterface) {
$property_definition = $property_definition->getItemDefinition();
$property_label = $property_definition->getLabel() ?: $property_label;
}
// If the property is complex too, recurse to find child properties.
if ($property_definition instanceof ComplexDataDefinitionInterface) {
$property_tokens = $this->getTokensFromComplexData($property_definition);
foreach ($property_tokens as $token => $label) {
$tokens[$property_name . ':' . $token] = count($property_tokens) > 1 ? ($property_label . ': ' . $label) : $property_label;
}
}
// Only expose references as tokens.
// @todo Consider to expose primitive and non-reference typed data
// definitions too, like strings, integers and dates. The current UI
// will not scale to that.
if ($property_definition instanceof DataReferenceDefinitionInterface) {
$tokens[$property_name] = $property_definition->getLabel();
}
}
return $tokens;
}
}

View file

@ -0,0 +1,180 @@
<?php
namespace Drupal\ctools\Wizard;
use Drupal\Core\DependencyInjection\ClassResolverInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\ctools\Event\WizardEvent;
use Drupal\user\SharedTempStoreFactory;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* The base class for all entity form wizards.
*/
abstract class EntityFormWizardBase extends FormWizardBase implements EntityFormWizardInterface {
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* @param \Drupal\user\SharedTempStoreFactory $tempstore
* Tempstore Factory for keeping track of values in each step of the
* wizard.
* @param \Drupal\Core\Form\FormBuilderInterface $builder
* The Form Builder.
* @param \Drupal\Core\DependencyInjection\ClassResolverInterface $class_resolver
* The class resolver.
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
* The event dispatcher.
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
* @param $tempstore_id
* The shared temp store factory collection name.
* @param null $machine_name
* The SharedTempStore key for our current wizard values.
* @param null $step
* The current active step of the wizard.
*/
public function __construct(SharedTempStoreFactory $tempstore, FormBuilderInterface $builder, ClassResolverInterface $class_resolver, EventDispatcherInterface $event_dispatcher, EntityManagerInterface $entity_manager, RouteMatchInterface $route_match, $tempstore_id, $machine_name = NULL, $step = NULL) {
$this->entityManager = $entity_manager;
parent::__construct($tempstore, $builder, $class_resolver, $event_dispatcher, $route_match, $tempstore_id, $machine_name, $step);
}
/**
* {@inheritdoc}
*/
public static function getParameters() {
return [
'tempstore' => \Drupal::service('user.shared_tempstore'),
'builder' => \Drupal::service('form_builder'),
'class_resolver' => \Drupal::service('class_resolver'),
'event_dispatcher' => \Drupal::service('event_dispatcher'),
'entity_manager' => \Drupal::service('entity.manager'),
];
}
/**
* {@inheritdoc}
*/
public function initValues() {
$storage = $this->entityManager->getStorage($this->getEntityType());
if ($this->getMachineName()) {
$values = $this->getTempstore()->get($this->getMachineName());
if (!$values) {
$entity = $storage->load($this->getMachineName());
$values[$this->getEntityType()] = $entity;
$values['id'] = $entity->id();
$values['label'] = $entity->label();
}
}
else {
$entity = $storage->create([]);
$values[$this->getEntityType()] = $entity;
}
$event = new WizardEvent($this, $values);
$this->dispatcher->dispatch(FormWizardInterface::LOAD_VALUES, $event);
return $event->getValues();
}
/**
* {@inheritdoc}
*/
public function finish(array &$form, FormStateInterface $form_state) {
$cached_values = $form_state->getTemporaryValue('wizard');
/** @var $entity \Drupal\Core\Entity\EntityInterface */
$entity = $cached_values[$this->getEntityType()];
$entity->set('id', $cached_values['id']);
$entity->set('label', $cached_values['label']);
$status = $entity->save();
$definition = $this->entityManager->getDefinition($this->getEntityType());
if ($status) {
drupal_set_message($this->t('Saved the %label @entity_type.', array(
'%label' => $entity->label(),
'@entity_type' => $definition->getLabel(),
)));
}
else {
drupal_set_message($this->t('The %label @entity_type was not saved.', array(
'%label' => $entity->label(),
'@entity_type' => $definition->getLabel(),
)));
}
$form_state->setRedirectUrl($entity->toUrl('collection'));
parent::finish($form, $form_state);
}
/**
* Helper function for generating label and id form elements.
*
* @param array $form
* @param \Drupal\Core\Form\FormStateInterface $form_state
*
* @return array
*/
protected function customizeForm(array $form, FormStateInterface $form_state) {
$form = parent::customizeForm($form, $form_state);
if ($this->machine_name) {
$entity = $this->entityManager->getStorage($this->getEntityType())
->load($this->machine_name);
}
else {
$entity = NULL;
}
$cached_values = $form_state->getTemporaryValue('wizard');
// If the entity already exists, allow for non-linear step interaction.
if ($entity) {
// Setup the step rendering theme element.
$prefix = [
'#theme' => ['ctools_wizard_trail_links'],
'#wizard' => $this,
'#cached_values' => $cached_values,
];
$form['#prefix'] = \Drupal::service('renderer')->render($prefix);
}
// Get the current form operation.
$operation = $this->getOperation($cached_values);
$operations = $this->getOperations($cached_values);
$default_operation = reset($operations);
if ($operation['form'] == $default_operation['form']) {
// Get the plugin definition of this entity.
$definition = $this->entityManager->getDefinition($this->getEntityType());
// Create id and label form elements.
$form['name'] = array(
'#type' => 'fieldset',
'#attributes' => array('class' => array('fieldset-no-legend')),
'#title' => $this->getWizardLabel(),
);
$form['name']['label'] = array(
'#type' => 'textfield',
'#title' => $this->getMachineLabel(),
'#required' => TRUE,
'#size' => 32,
'#default_value' => !empty($cached_values['label']) ? $cached_values['label'] : '',
'#maxlength' => 255,
'#disabled' => !empty($cached_values['label']),
);
$form['name']['id'] = array(
'#type' => 'machine_name',
'#maxlength' => 128,
'#machine_name' => array(
'source' => array('name', 'label'),
'exists' => $this->exists(),
),
'#description' => $this->t('A unique machine-readable name for this @entity_type. It must only contain lowercase letters, numbers, and underscores.', ['@entity_type' => $definition->getLabel()]),
'#default_value' => !empty($cached_values['id']) ? $cached_values['id'] : '',
'#disabled' => !empty($cached_values['id']),
);
}
return $form;
}
}

View file

@ -0,0 +1,39 @@
<?php
namespace Drupal\ctools\Wizard;
/**
* Form wizard interface for use with entities.
*/
interface EntityFormWizardInterface extends FormWizardInterface {
/**
* The fieldset #title for your label & machine name elements.
*
* @return string
*/
public function getWizardLabel();
/**
* The form element #title for your unique identifier label.
*
* @return string
*/
public function getMachineLabel();
/**
* The machine name of the entity type.
*
* @return string
*/
public function getEntityType();
/**
* A method for determining if this entity already exists.
*
* @return callable
* The callable to pass the id to via typical machine_name form element.
*/
public function exists();
}

View file

@ -0,0 +1,465 @@
<?php
namespace Drupal\ctools\Wizard;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\CloseModalDialogCommand;
use Drupal\Core\DependencyInjection\ClassResolverInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Form\FormInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;
use Drupal\ctools\Ajax\OpenModalWizardCommand;
use Drupal\ctools\Event\WizardEvent;
use Drupal\user\SharedTempStoreFactory;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* The base class for all form wizard.
*/
abstract class FormWizardBase extends FormBase implements FormWizardInterface {
/**
* Tempstore Factory for keeping track of values in each step of the wizard.
*
* @var \Drupal\user\SharedTempStoreFactory
*/
protected $tempstore;
/**
* The Form Builder.
*
* @var \Drupal\Core\Form\FormBuilderInterface
*/
protected $builder;
/**
* The class resolver.
*
* @var \Drupal\Core\DependencyInjection\ClassResolverInterface;
*/
protected $classResolver;
/**
* The event dispatcher.
*
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
protected $dispatcher;
/**
* The shared temp store factory collection name.
*
* @var string
*/
protected $tempstore_id;
/**
* The SharedTempStore key for our current wizard values.
*
* @var string|NULL
*/
protected $machine_name;
/**
* The current active step of the wizard.
*
* @var string|NULL
*/
protected $step;
/**
* @param \Drupal\user\SharedTempStoreFactory $tempstore
* Tempstore Factory for keeping track of values in each step of the
* wizard.
* @param \Drupal\Core\Form\FormBuilderInterface $builder
* The Form Builder.
* @param \Drupal\Core\DependencyInjection\ClassResolverInterface $class_resolver
* The class resolver.
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
* The event dispatcher.
* @param $tempstore_id
* The shared temp store factory collection name.
* @param null $machine_name
* The SharedTempStore key for our current wizard values.
* @param null $step
* The current active step of the wizard.
*/
public function __construct(SharedTempStoreFactory $tempstore, FormBuilderInterface $builder, ClassResolverInterface $class_resolver, EventDispatcherInterface $event_dispatcher, RouteMatchInterface $route_match, $tempstore_id, $machine_name = NULL, $step = NULL) {
$this->tempstore = $tempstore;
$this->builder = $builder;
$this->classResolver = $class_resolver;
$this->dispatcher = $event_dispatcher;
$this->routeMatch = $route_match;
$this->tempstore_id = $tempstore_id;
$this->machine_name = $machine_name;
$this->step = $step;
}
/**
* {@inheritdoc}
*/
public static function getParameters() {
return [
'tempstore' => \Drupal::service('user.shared_tempstore'),
'builder' => \Drupal::service('form_builder'),
'class_resolver' => \Drupal::service('class_resolver'),
'event_dispatcher' => \Drupal::service('event_dispatcher'),
];
}
/**
* {@inheritdoc}
*/
public function initValues() {
$values = [];
$event = new WizardEvent($this, $values);
$this->dispatcher->dispatch(FormWizardInterface::LOAD_VALUES, $event);
return $event->getValues();
}
/**
* {@inheritdoc}
*/
public function getTempstoreId() {
return $this->tempstore_id;
}
/**
* {@inheritdoc}
*/
public function getTempstore() {
return $this->tempstore->get($this->getTempstoreId());
}
/**
* {@inheritdoc}
*/
public function getMachineName() {
return $this->machine_name;
}
/**
* {@inheritdoc}
*/
public function getStep($cached_values) {
if (!$this->step) {
$operations = $this->getOperations($cached_values);
$steps = array_keys($operations);
$this->step = reset($steps);
}
return $this->step;
}
/**
* {@inheritdoc}
*/
public function getOperation($cached_values) {
$operations = $this->getOperations($cached_values);
$step = $this->getStep($cached_values);
if (!empty($operations[$step])) {
return $operations[$step];
}
$operation = reset($operations);
return $operation;
}
/**
* The translated text of the "Next" button's text.
*
* @return string
*/
public function getNextOp() {
return $this->t('Next');
}
/**
* {@inheritdoc}
*/
public function getNextParameters($cached_values) {
// Get the steps by key.
$operations = $this->getOperations($cached_values);
$steps = array_keys($operations);
// Get the steps after the current step.
$after = array_slice($operations, array_search($this->getStep($cached_values), $steps) + 1);
// Get the steps after the current step by key.
$after_keys = array_keys($after);
$step = reset($after_keys);
if (!$step) {
$keys = array_keys($operations);
$step = end($keys);
}
return [
'machine_name' => $this->getMachineName(),
'step' => $step,
'js' => 'nojs',
];
}
/**
* {@inheritdoc}
*/
public function getPreviousParameters($cached_values) {
$operations = $this->getOperations($cached_values);
$step = $this->getStep($cached_values);
// Get the steps by key.
$steps = array_keys($operations);
// Get the steps before the current step.
$before = array_slice($operations, 0, array_search($step, $steps));
// Get the steps before the current step by key.
$before = array_keys($before);
// Reverse the steps for easy access to the next step.
$before_steps = array_reverse($before);
$step = reset($before_steps);
return [
'machine_name' => $this->getMachineName(),
'step' => $step,
'js' => 'nojs',
];
}
/**
* {@inheritdoc}
*/
public function getFormId() {
if (!$this->getMachineName() || !$this->getTempstore()->get($this->getMachineName())) {
$cached_values = $this->initValues();
}
else {
$cached_values = $this->getTempstore()->get($this->getMachineName());
}
$operation = $this->getOperation($cached_values);
/* @var $operation \Drupal\Core\Form\FormInterface */
$operation = $this->classResolver->getInstanceFromDefinition($operation['form']);
return $operation->getFormId();
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$cached_values = $form_state->getTemporaryValue('wizard');
// Get the current form operation.
$operation = $this->getOperation($cached_values);
$form = $this->customizeForm($form, $form_state);
/* @var $formClass \Drupal\Core\Form\FormInterface */
$formClass = $this->classResolver->getInstanceFromDefinition($operation['form']);
// Pass include any custom values for this operation.
if (!empty($operation['values'])) {
$cached_values = array_merge($cached_values, $operation['values']);
$form_state->setTemporaryValue('wizard', $cached_values);
}
// Build the form.
$form = $formClass->buildForm($form, $form_state);
if (isset($operation['title'])) {
$form['#title'] = $operation['title'];
}
$form['actions'] = $this->actions($formClass, $form_state);
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// Only perform this logic if we're moving to the next page. This prevents
// the loss of cached values on ajax submissions.
if ((string)$form_state->getValue('op') == (string)$this->getNextOp()) {
$cached_values = $form_state->getTemporaryValue('wizard');
if ($form_state->hasValue('label')) {
$cached_values['label'] = $form_state->getValue('label');
}
if ($form_state->hasValue('id')) {
$cached_values['id'] = $form_state->getValue('id');
}
if (is_null($this->machine_name) && !empty($cached_values['id'])) {
$this->machine_name = $cached_values['id'];
}
$this->getTempstore()->set($this->getMachineName(), $cached_values);
if (!$form_state->get('ajax')) {
$form_state->setRedirect($this->getRouteName(), $this->getNextParameters($cached_values));
}
}
}
/**
* {@inheritdoc}
*/
public function populateCachedValues(array &$form, FormStateInterface $form_state) {
$cached_values = $this->getTempstore()->get($this->getMachineName());
if (!$cached_values) {
$cached_values = $form_state->getTemporaryValue('wizard');
if (!$cached_values) {
$cached_values = $this->initValues();
$form_state->setTemporaryValue('wizard', $cached_values);
}
}
}
/**
* {@inheritdoc}
*/
public function previous(array &$form, FormStateInterface $form_state) {
$cached_values = $form_state->getTemporaryValue('wizard');
$form_state->setRedirect($this->getRouteName(), $this->getPreviousParameters($cached_values));
}
/**
* {@inheritdoc}
*/
public function finish(array &$form, FormStateInterface $form_state) {
$this->getTempstore()->delete($this->getMachineName());
}
/**
* Helper function for generating default form elements.
*
* @param array $form
* @param \Drupal\Core\Form\FormStateInterface $form_state
*
* @return array
*/
protected function customizeForm(array $form, FormStateInterface $form_state) {
// Setup the step rendering theme element.
$prefix = [
'#theme' => ['ctools_wizard_trail'],
'#wizard' => $this,
'#cached_values' => $form_state->getTemporaryValue('wizard'),
];
// @todo properly inject the renderer.
$form['#prefix'] = \Drupal::service('renderer')->render($prefix);
return $form;
}
/**
* Generates action elements for navigating between the operation steps.
*
* @param \Drupal\Core\Form\FormInterface $form_object
* The current operation form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current form state.
*
* @return array
*/
protected function actions(FormInterface $form_object, FormStateInterface $form_state) {
$cached_values = $form_state->getTemporaryValue('wizard');
$operations = $this->getOperations($cached_values);
$step = $this->getStep($cached_values);
$operation = $operations[$step];
$steps = array_keys($operations);
// Slice to find the operations that occur before the current operation.
$before = array_slice($operations, 0, array_search($step, $steps));
// Slice to find the operations that occur after the current operation.
$after = array_slice($operations, array_search($step, $steps) + 1);
$actions = [
'submit' => [
'#type' => 'submit',
'#value' => $this->t('Next'),
'#button_type' => 'primary',
'#validate' => [
'::populateCachedValues',
[$form_object, 'validateForm'],
],
'#submit' => [
[$form_object, 'submitForm'],
],
],
];
// Add any submit or validate functions for the step and the global ones.
if (isset($operation['validate'])) {
$actions['submit']['#validate'] = array_merge($actions['submit']['#validate'], $operation['validate']);
}
$actions['submit']['#validate'][] = '::validateForm';
if (isset($operation['submit'])) {
$actions['submit']['#submit'] = array_merge($actions['submit']['#submit'], $operation['submit']);
}
$actions['submit']['#submit'][] = '::submitForm';
if ($form_state->get('ajax')) {
// Ajax submissions need to submit to the current step, not "next".
$parameters = $this->getNextParameters($cached_values);
$parameters['step'] = $this->getStep($cached_values);
$actions['submit']['#ajax'] = [
'callback' => '::ajaxSubmit',
'url' => Url::fromRoute($this->getRouteName(), $parameters),
'options' => ['query' => \Drupal::request()->query->all() + [FormBuilderInterface::AJAX_FORM_REQUEST => TRUE]],
];
}
// If there are steps before this one, label the button "previous"
// otherwise do not display a button.
if ($before) {
$actions['previous'] = array(
'#type' => 'submit',
'#value' => $this->t('Previous'),
'#validate' => array(
array($this, 'populateCachedValues'),
),
'#submit' => array(
array($this, 'previous'),
),
'#limit_validation_errors' => array(),
'#weight' => -10,
);
if ($form_state->get('ajax')) {
// Ajax submissions need to submit to the current step, not "previous".
$parameters = $this->getPreviousParameters($cached_values);
$parameters['step'] = $this->getStep($cached_values);
$actions['previous']['#ajax'] = [
'callback' => '::ajaxPrevious',
'url' => Url::fromRoute($this->getRouteName(), $parameters),
'options' => ['query' => \Drupal::request()->query->all() + [FormBuilderInterface::AJAX_FORM_REQUEST => TRUE]],
];
}
}
// If there are not steps after this one, label the button "Finish".
if (!$after) {
$actions['submit']['#value'] = $this->t('Finish');
$actions['submit']['#submit'][] = array($this, 'finish');
if ($form_state->get('ajax')) {
$actions['submit']['#ajax']['callback'] = [$this, 'ajaxFinish'];
}
}
return $actions;
}
public function ajaxSubmit(array $form, FormStateInterface $form_state) {
$cached_values = $form_state->getTemporaryValue('wizard');
$response = new AjaxResponse();
$parameters = $this->getNextParameters($cached_values);
$response->addCommand(new OpenModalWizardCommand($this, $this->getTempstoreId(), $parameters));
return $response;
}
public function ajaxPrevious(array $form, FormStateInterface $form_state) {
$cached_values = $form_state->getTemporaryValue('wizard');
$response = new AjaxResponse();
$parameters = $this->getPreviousParameters($cached_values);
$response->addCommand(new OpenModalWizardCommand($this, $this->getTempstoreId(), $parameters));
return $response;
}
public function ajaxFinish(array $form, FormStateInterface $form_state) {
$response = new AjaxResponse();
$response->addCommand(new CloseModalDialogCommand());
return $response;
}
public function getRouteName() {
return $this->routeMatch->getRouteName();
}
}

View file

@ -0,0 +1,182 @@
<?php
namespace Drupal\ctools\Wizard;
use Drupal\Core\Form\FormInterface;
use Drupal\Core\Form\FormStateInterface;
/**
* Form wizard interface.
*/
interface FormWizardInterface extends FormInterface {
/**
* Constant value for wizard load event.
*/
const LOAD_VALUES = 'wizard.load';
/**
* Return an array of parameters required to construct this wizard.
*
* @return array
*/
public static function getParameters();
/**
* Initialize wizard values.
*
* return mixed.
*/
public function initValues();
/**
* The shared temp store factory collection name.
*
* @return string
*/
public function getTempstoreId();
/**
* The active SharedTempStore for this wizard.
*
* @return \Drupal\user\SharedTempStore
*/
public function getTempstore();
/**
* The SharedTempStore key for our current wizard values.
*
* @return null|string
*/
public function getMachineName();
/**
* Retrieve the current active step of the wizard.
*
* This will return the first step of the wizard if no step has been set.
*
* @param mixed $cached_values
* The values returned by $this->getTempstore()->get($this->getMachineName());
*
* @return string
*/
public function getStep($cached_values);
/**
* Retrieve a list of FormInterface classes by their step key in the wizard.
*
* @param mixed $cached_values
* The values returned by $this->getTempstore()->get($this->getMachineName()); *
*
* @return array
* An associative array keyed on the step name with an array value with the
* following keys:
* - title (string): Human-readable title of the step.
* - form (string): Fully-qualified class name of the form for this step.
* - values (array): Optional array of cached values to override when on
* this step.
* - validate (array): Optional array of callables to be called when this
* step is validated.
* - submit (array): Optional array of callables to be called when this
* step is submitted.
*/
public function getOperations($cached_values);
/**
* Retrieve the current Operation.
*
* @param mixed $cached_values
* The values returned by $this->getTempstore()->get($this->getMachineName());
*
* @return string
* The class name to instantiate.
*/
public function getOperation($cached_values);
/**
* The name of the route to which forward or backwards steps redirect.
*
* @return string
*/
public function getRouteName();
/**
* The Route parameters for a 'next' step.
*
* If your route requires more than machine_name and step keys, override and
* extend this method as needed.
*
* @param mixed $cached_values
* The values returned by $this->getTempstore()->get($this->getMachineName());
*
* @return array
* An array keyed by:
* machine_name
* step
*/
public function getNextParameters($cached_values);
/**
* The Route parameters for a 'previous' step.
*
* If your route requires more than machine_name and step keys, override and
* extend this method as needed.
*
* @param mixed $cached_values
* The values returned by $this->getTempstore()->get($this->getMachineName());
*
* @return array
* An array keyed by:
* machine_name
* step
*/
public function getPreviousParameters($cached_values);
/**
* Form validation handler that populates the cached values from tempstore.
*
* Temporary values are only available for a single page load so form
* submission will lose all the values. This was we reload and provide them
* to the validate and submit process.
*
* @param array $form
* Drupal form array
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The initial form state before validation or submission of the steps.
*/
public function populateCachedValues(array &$form, FormStateInterface $form_state);
/**
* Form submit handler to step backwards in the wizard.
*
* "Next" steps are handled by \Drupal\Core\Form\FormInterface::submitForm().
*
* @param array $form
* Drupal form array
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current form state of the wizard. This will not contain values from
* the current step since the previous button does not actually submit
* those values.
*/
public function previous(array &$form, FormStateInterface $form_state);
/**
* Form submit handler for finalizing the wizard values.
*
* If you need to generate an entity or save config or raw table data
* subsequent to your form wizard, this is the responsible method.
*
* @param array $form
* Drupal form array
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The final form state of the wizard.
*/
public function finish(array &$form, FormStateInterface $form_state);
public function ajaxSubmit(array $form, FormStateInterface $form_state);
public function ajaxPrevious(array $form, FormStateInterface $form_state);
public function ajaxFinish(array $form, FormStateInterface $form_state);
}

View file

@ -0,0 +1,129 @@
<?php
namespace Drupal\ctools\Wizard;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Form\FormState;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
class WizardFactory implements WizardFactoryInterface {
/**
* The Form Builder.
*
* @var \Drupal\Core\Form\FormBuilderInterface
*/
protected $builder;
/**
* The event dispatcher.
*
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
protected $dispatcher;
/**
* @param \Drupal\Core\Form\FormBuilderInterface $form_builder
* The form builder.
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
* The event dispatcher.
*/
public function __construct(FormBuilderInterface $form_builder, EventDispatcherInterface $event_dispatcher) {
$this->builder = $form_builder;
$this->dispatcher = $event_dispatcher;
}
/**
* {@inheritdoc}
*/
public function getWizardForm(FormWizardInterface $wizard, array $parameters = [], $ajax = FALSE) {
$form_state = $this->getFormState($wizard, $parameters, $ajax);
$form = $this->builder->buildForm($wizard, $form_state);
if ($ajax) {
$form['#attached']['library'][] = 'core/drupal.dialog.ajax';
$status_messages = array('#type' => 'status_messages');
// @todo properly inject the renderer. Core should really be doing this work.
if ($messages = \Drupal::service('renderer')->renderRoot($status_messages)) {
if (!empty($form['#prefix'])) {
// Form prefix is expected to be a string. Prepend the messages to
// that string.
$form['#prefix'] = '<div class="wizard-messages">' . $messages . '</div>' . $form['#prefix'];
}
}
}
return $form;
}
/**
* @param string $class
* A class name implementing FormWizardInterface.
* @param array $parameters
* The array of parameters specific to this wizard.
*
* @return \Drupal\ctools\Wizard\FormWizardInterface
*/
public function createWizard($class, array $parameters) {
$arguments = [];
$reflection = new \ReflectionClass($class);
$constructor = $reflection->getMethod('__construct');
foreach ($constructor->getParameters() as $parameter) {
if (array_key_exists($parameter->name, $parameters)) {
$arguments[] = $parameters[$parameter->name];
}
elseif ($parameter->isDefaultValueAvailable()) {
$arguments[] = $parameter->getDefaultValue();
}
}
/** @var $wizard \Drupal\ctools\Wizard\FormWizardInterface */
$wizard = $reflection->newInstanceArgs($arguments);
return $wizard;
}
/**
* Get the wizard form state.
*
* @param \Drupal\ctools\Wizard\FormWizardInterface $wizard
* The form wizard.
* @param array $parameters
* The array of parameters specific to this wizard.
* @param bool $ajax
*
* @return \Drupal\Core\Form\FormState
*/
public function getFormState(FormWizardInterface $wizard, array $parameters, $ajax = FALSE) {
$form_state = new FormState();
// If a wizard has no values, initialize them.
if (!$wizard->getMachineName() || !$wizard->getTempstore()->get($wizard->getMachineName())) {
$cached_values = $wizard->initValues();
// Save the cached values that were initialized.
if ($wizard->getMachineName()) {
$wizard->getTempstore()->set($wizard->getMachineName(), $cached_values);
}
}
else {
$cached_values = $wizard->getTempstore()->get($wizard->getMachineName());
}
$form_state->setTemporaryValue('wizard', $cached_values);
$form_state->set('ajax', $ajax);
$parameters['form'] = [];
$parameters['form_state'] = $form_state;
$method = new \ReflectionMethod($wizard, 'buildForm');
$arguments = [];
foreach ($method->getParameters() as $parameter) {
if (array_key_exists($parameter->name, $parameters)) {
$arguments[] = $parameters[$parameter->name];
}
elseif ($parameter->isDefaultValueAvailable()) {
$arguments[] = $parameter->getDefaultValue();
}
}
unset($parameters['form'], $parameters['form_state']);
// Remove $form and $form_state from the arguments, and re-index them.
unset($arguments[0], $arguments[1]);
$form_state->addBuildInfo('args', array_values($arguments));
return $form_state;
}
}

View file

@ -0,0 +1,42 @@
<?php
namespace Drupal\ctools\Wizard;
interface WizardFactoryInterface {
/**
* Get the wizard form.
*
* @param FormWizardInterface $wizard
* The form wizard
* @param array $parameters
* The array of default parameters specific to this wizard.
* @param bool $ajax
* Whether or not this wizard is displayed via ajax modals.
*
* @return array
*/
public function getWizardForm(FormWizardInterface $wizard, array $parameters = [], $ajax = FALSE);
/**
* @param string $class
* A class name implementing FormWizardInterface.
* @param array $parameters
* The array of parameters specific to this wizard.
*
* @return \Drupal\ctools\Wizard\FormWizardInterface
*/
public function createWizard($class, array $parameters);
/**
* Get the wizard form state.
*
* @param \Drupal\ctools\Wizard\FormWizardInterface $wizard
* The form wizard.
* @param array $parameters
* The array of parameters specific to this wizard.
* @param bool $ajax
*
* @return \Drupal\Core\Form\FormState
*/
public function getFormState(FormWizardInterface $wizard, array $parameters, $ajax = FALSE);
}