e537ef804fe2da27e204eaff39dcb07c88842e9c
[yaffs-website] / Wizard / FormWizardBase.php
1 <?php
2
3 namespace Drupal\ctools\Wizard;
4
5 use Drupal\Core\Ajax\AjaxResponse;
6 use Drupal\Core\Ajax\CloseModalDialogCommand;
7 use Drupal\Core\DependencyInjection\ClassResolverInterface;
8 use Drupal\Core\Form\FormBase;
9 use Drupal\Core\Form\FormBuilderInterface;
10 use Drupal\Core\Form\FormInterface;
11 use Drupal\Core\Form\FormStateInterface;
12 use Drupal\Core\Routing\RouteMatchInterface;
13 use Drupal\Core\Url;
14 use Drupal\ctools\Ajax\OpenModalWizardCommand;
15 use Drupal\ctools\Event\WizardEvent;
16 use Drupal\user\SharedTempStoreFactory;
17 use Symfony\Component\EventDispatcher\EventDispatcherInterface;
18
19 /**
20  * The base class for all form wizard.
21  */
22 abstract class FormWizardBase extends FormBase implements FormWizardInterface {
23
24   /**
25    * Tempstore Factory for keeping track of values in each step of the wizard.
26    *
27    * @var \Drupal\user\SharedTempStoreFactory
28    */
29   protected $tempstore;
30
31   /**
32    * The Form Builder.
33    *
34    * @var \Drupal\Core\Form\FormBuilderInterface
35    */
36   protected $builder;
37
38   /**
39    * The class resolver.
40    *
41    * @var \Drupal\Core\DependencyInjection\ClassResolverInterface;
42    */
43   protected $classResolver;
44
45   /**
46    * The event dispatcher.
47    *
48    * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
49    */
50   protected $dispatcher;
51
52   /**
53    * The shared temp store factory collection name.
54    *
55    * @var string
56    */
57   protected $tempstore_id;
58
59   /**
60    * The SharedTempStore key for our current wizard values.
61    *
62    * @var string|NULL
63    */
64   protected $machine_name;
65
66   /**
67    * The current active step of the wizard.
68    *
69    * @var string|NULL
70    */
71   protected $step;
72
73   /**
74    * @param \Drupal\user\SharedTempStoreFactory $tempstore
75    *   Tempstore Factory for keeping track of values in each step of the
76    *   wizard.
77    * @param \Drupal\Core\Form\FormBuilderInterface $builder
78    *   The Form Builder.
79    * @param \Drupal\Core\DependencyInjection\ClassResolverInterface $class_resolver
80    *   The class resolver.
81    * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
82    *   The event dispatcher.
83    * @param $tempstore_id
84    *   The shared temp store factory collection name.
85    * @param null $machine_name
86    *   The SharedTempStore key for our current wizard values.
87    * @param null $step
88    *   The current active step of the wizard.
89    */
90   public function __construct(SharedTempStoreFactory $tempstore, FormBuilderInterface $builder, ClassResolverInterface $class_resolver, EventDispatcherInterface $event_dispatcher, RouteMatchInterface $route_match, $tempstore_id, $machine_name = NULL, $step = NULL) {
91     $this->tempstore = $tempstore;
92     $this->builder = $builder;
93     $this->classResolver = $class_resolver;
94     $this->dispatcher = $event_dispatcher;
95     $this->routeMatch = $route_match;
96     $this->tempstore_id = $tempstore_id;
97     $this->machine_name = $machine_name;
98     $this->step = $step;
99   }
100
101   /**
102    * {@inheritdoc}
103    */
104   public static function getParameters() {
105     return [
106       'tempstore' => \Drupal::service('user.shared_tempstore'),
107       'builder' => \Drupal::service('form_builder'),
108       'class_resolver' => \Drupal::service('class_resolver'),
109       'event_dispatcher' => \Drupal::service('event_dispatcher'),
110     ];
111   }
112
113   /**
114    * {@inheritdoc}
115    */
116   public function initValues() {
117     $values = [];
118     $event = new WizardEvent($this, $values);
119     $this->dispatcher->dispatch(FormWizardInterface::LOAD_VALUES, $event);
120     return $event->getValues();
121   }
122
123   /**
124    * {@inheritdoc}
125    */
126   public function getTempstoreId() {
127     return $this->tempstore_id;
128   }
129
130   /**
131    * {@inheritdoc}
132    */
133   public function getTempstore() {
134     return $this->tempstore->get($this->getTempstoreId());
135   }
136
137   /**
138    * {@inheritdoc}
139    */
140   public function getMachineName() {
141     return $this->machine_name;
142   }
143
144   /**
145    * {@inheritdoc}
146    */
147   public function getStep($cached_values) {
148     if (!$this->step) {
149       $operations = $this->getOperations($cached_values);
150       $steps = array_keys($operations);
151       $this->step = reset($steps);
152     }
153     return $this->step;
154   }
155
156   /**
157    * {@inheritdoc}
158    */
159   public function getOperation($cached_values) {
160     $operations = $this->getOperations($cached_values);
161     $step = $this->getStep($cached_values);
162     if (!empty($operations[$step])) {
163       return $operations[$step];
164     }
165     $operation = reset($operations);
166     return $operation;
167   }
168
169   /**
170    * The translated text of the "Next" button's text.
171    *
172    * @return string
173    */
174   public function getNextOp() {
175     return $this->t('Next');
176   }
177
178   /**
179    * {@inheritdoc}
180    */
181   public function getNextParameters($cached_values) {
182     // Get the steps by key.
183     $operations = $this->getOperations($cached_values);
184     $steps = array_keys($operations);
185     // Get the steps after the current step.
186     $after = array_slice($operations, array_search($this->getStep($cached_values), $steps) + 1);
187     // Get the steps after the current step by key.
188     $after_keys = array_keys($after);
189     $step = reset($after_keys);
190     if (!$step) {
191       $keys = array_keys($operations);
192       $step = end($keys);
193     }
194     return [
195       'machine_name' => $this->getMachineName(),
196       'step' => $step,
197       'js' => 'nojs',
198     ];
199   }
200
201   /**
202    * {@inheritdoc}
203    */
204   public function getPreviousParameters($cached_values) {
205     $operations = $this->getOperations($cached_values);
206     $step = $this->getStep($cached_values);
207
208     // Get the steps by key.
209     $steps = array_keys($operations);
210     // Get the steps before the current step.
211     $before = array_slice($operations, 0, array_search($step, $steps));
212     // Get the steps before the current step by key.
213     $before = array_keys($before);
214     // Reverse the steps for easy access to the next step.
215     $before_steps = array_reverse($before);
216     $step = reset($before_steps);
217     return [
218       'machine_name' => $this->getMachineName(),
219       'step' => $step,
220       'js' => 'nojs',
221     ];
222   }
223
224   /**
225    * {@inheritdoc}
226    */
227   public function getFormId() {
228     if (!$this->getMachineName() || !$this->getTempstore()->get($this->getMachineName())) {
229       $cached_values = $this->initValues();
230     }
231     else {
232       $cached_values = $this->getTempstore()->get($this->getMachineName());
233     }
234     $operation = $this->getOperation($cached_values);
235     /* @var $operation \Drupal\Core\Form\FormInterface */
236     $operation = $this->classResolver->getInstanceFromDefinition($operation['form']);
237     return $operation->getFormId();
238   }
239
240   /**
241    * {@inheritdoc}
242    */
243   public function buildForm(array $form, FormStateInterface $form_state) {
244     $cached_values = $form_state->getTemporaryValue('wizard');
245     // Get the current form operation.
246     $operation = $this->getOperation($cached_values);
247     $form = $this->customizeForm($form, $form_state);
248     /* @var $formClass \Drupal\Core\Form\FormInterface */
249     $formClass = $this->classResolver->getInstanceFromDefinition($operation['form']);
250     // Pass include any custom values for this operation.
251     if (!empty($operation['values'])) {
252       $cached_values = array_merge($cached_values, $operation['values']);
253       $form_state->setTemporaryValue('wizard', $cached_values);
254     }
255     // Build the form.
256     $form = $formClass->buildForm($form, $form_state);
257     if (isset($operation['title'])) {
258       $form['#title'] = $operation['title'];
259     }
260     $form['actions'] = $this->actions($formClass, $form_state);
261     return $form;
262   }
263
264   /**
265    * {@inheritdoc}
266    */
267   public function validateForm(array &$form, FormStateInterface $form_state) {}
268
269   /**
270    * {@inheritdoc}
271    */
272   public function submitForm(array &$form, FormStateInterface $form_state) {
273     // Only perform this logic if we're moving to the next page. This prevents
274     // the loss of cached values on ajax submissions.
275     if ((string)$form_state->getValue('op') == (string)$this->getNextOp()) {
276       $cached_values = $form_state->getTemporaryValue('wizard');
277       if ($form_state->hasValue('label')) {
278         $cached_values['label'] = $form_state->getValue('label');
279       }
280       if ($form_state->hasValue('id')) {
281         $cached_values['id'] = $form_state->getValue('id');
282       }
283       if (is_null($this->machine_name) && !empty($cached_values['id'])) {
284         $this->machine_name = $cached_values['id'];
285       }
286       $this->getTempstore()->set($this->getMachineName(), $cached_values);
287       if (!$form_state->get('ajax')) {
288         $form_state->setRedirect($this->getRouteName(), $this->getNextParameters($cached_values));
289       }
290     }
291   }
292
293   /**
294    * {@inheritdoc}
295    */
296   public function populateCachedValues(array &$form, FormStateInterface $form_state) {
297     $cached_values = $this->getTempstore()->get($this->getMachineName());
298     if (!$cached_values) {
299       $cached_values = $form_state->getTemporaryValue('wizard');
300       if (!$cached_values) {
301         $cached_values = $this->initValues();
302         $form_state->setTemporaryValue('wizard', $cached_values);
303       }
304     }
305   }
306
307   /**
308    * {@inheritdoc}
309    */
310   public function previous(array &$form, FormStateInterface $form_state) {
311     $cached_values = $form_state->getTemporaryValue('wizard');
312     $form_state->setRedirect($this->getRouteName(), $this->getPreviousParameters($cached_values));
313   }
314
315   /**
316    * {@inheritdoc}
317    */
318   public function finish(array &$form, FormStateInterface $form_state) {
319     $this->getTempstore()->delete($this->getMachineName());
320   }
321
322   /**
323    * Helper function for generating default form elements.
324    *
325    * @param array $form
326    * @param \Drupal\Core\Form\FormStateInterface $form_state
327    *
328    * @return array
329    */
330   protected function customizeForm(array $form, FormStateInterface $form_state) {
331     // Setup the step rendering theme element.
332     $prefix = [
333       '#theme' => ['ctools_wizard_trail'],
334       '#wizard' => $this,
335       '#cached_values' => $form_state->getTemporaryValue('wizard'),
336     ];
337     // @todo properly inject the renderer.
338     $form['#prefix'] = \Drupal::service('renderer')->render($prefix);
339     return $form;
340   }
341
342   /**
343    * Generates action elements for navigating between the operation steps.
344    *
345    * @param \Drupal\Core\Form\FormInterface $form_object
346    *   The current operation form.
347    * @param \Drupal\Core\Form\FormStateInterface $form_state
348    *   The current form state.
349    *
350    * @return array
351    */
352   protected function actions(FormInterface $form_object, FormStateInterface $form_state) {
353     $cached_values = $form_state->getTemporaryValue('wizard');
354     $operations = $this->getOperations($cached_values);
355     $step = $this->getStep($cached_values);
356     $operation = $operations[$step];
357
358     $steps = array_keys($operations);
359     // Slice to find the operations that occur before the current operation.
360     $before = array_slice($operations, 0, array_search($step, $steps));
361     // Slice to find the operations that occur after the current operation.
362     $after = array_slice($operations, array_search($step, $steps) + 1);
363
364     $actions = [
365       'submit' => [
366         '#type' => 'submit',
367         '#value' => $this->t('Next'),
368         '#button_type' => 'primary',
369         '#validate' => [
370           '::populateCachedValues',
371           [$form_object, 'validateForm'],
372         ],
373         '#submit' => [
374           [$form_object, 'submitForm'],
375         ],
376       ],
377     ];
378
379     // Add any submit or validate functions for the step and the global ones.
380     if (isset($operation['validate'])) {
381       $actions['submit']['#validate'] = array_merge($actions['submit']['#validate'], $operation['validate']);
382     }
383     $actions['submit']['#validate'][] = '::validateForm';
384     if (isset($operation['submit'])) {
385       $actions['submit']['#submit'] = array_merge($actions['submit']['#submit'], $operation['submit']);
386     }
387     $actions['submit']['#submit'][] = '::submitForm';
388
389     if ($form_state->get('ajax')) {
390       // Ajax submissions need to submit to the current step, not "next".
391       $parameters = $this->getNextParameters($cached_values);
392       $parameters['step'] = $this->getStep($cached_values);
393       $actions['submit']['#ajax'] = [
394         'callback' => '::ajaxSubmit',
395         'url' => Url::fromRoute($this->getRouteName(), $parameters),
396         'options' => ['query' => \Drupal::request()->query->all() + [FormBuilderInterface::AJAX_FORM_REQUEST => TRUE]],
397       ];
398     }
399
400     // If there are steps before this one, label the button "previous"
401     // otherwise do not display a button.
402     if ($before) {
403       $actions['previous'] = array(
404         '#type' => 'submit',
405         '#value' => $this->t('Previous'),
406         '#validate' => array(
407           array($this, 'populateCachedValues'),
408         ),
409         '#submit' => array(
410           array($this, 'previous'),
411         ),
412         '#limit_validation_errors' => array(),
413         '#weight' => -10,
414       );
415       if ($form_state->get('ajax')) {
416         // Ajax submissions need to submit to the current step, not "previous".
417         $parameters = $this->getPreviousParameters($cached_values);
418         $parameters['step'] = $this->getStep($cached_values);
419         $actions['previous']['#ajax'] = [
420           'callback' => '::ajaxPrevious',
421           'url' => Url::fromRoute($this->getRouteName(), $parameters),
422           'options' => ['query' => \Drupal::request()->query->all() + [FormBuilderInterface::AJAX_FORM_REQUEST => TRUE]],
423         ];
424       }
425     }
426
427     // If there are not steps after this one, label the button "Finish".
428     if (!$after) {
429       $actions['submit']['#value'] = $this->t('Finish');
430       $actions['submit']['#submit'][] = array($this, 'finish');
431       if ($form_state->get('ajax')) {
432         $actions['submit']['#ajax']['callback'] = [$this, 'ajaxFinish'];
433       }
434     }
435
436     return $actions;
437   }
438
439   public function ajaxSubmit(array $form, FormStateInterface $form_state) {
440     $cached_values = $form_state->getTemporaryValue('wizard');
441     $response = new AjaxResponse();
442     $parameters = $this->getNextParameters($cached_values);
443     $response->addCommand(new OpenModalWizardCommand($this, $this->getTempstoreId(), $parameters));
444     return $response;
445   }
446
447   public function ajaxPrevious(array $form, FormStateInterface $form_state) {
448     $cached_values = $form_state->getTemporaryValue('wizard');
449     $response = new AjaxResponse();
450     $parameters = $this->getPreviousParameters($cached_values);
451     $response->addCommand(new OpenModalWizardCommand($this, $this->getTempstoreId(), $parameters));
452     return $response;
453   }
454
455   public function ajaxFinish(array $form, FormStateInterface $form_state) {
456     $response = new AjaxResponse();
457     $response->addCommand(new CloseModalDialogCommand());
458     return $response;
459   }
460
461   public function getRouteName() {
462     return $this->routeMatch->getRouteName();
463   }
464
465 }