PluginProbe ʕ •ᴥ•ʔ
VikAppointments Services Booking Calendar / 1.2.21
VikAppointments Services Booking Calendar v1.2.21
1.2.21 1.2.20 trunk 1.2.17 1.2.18 1.2.19
vikappointments / site / helpers / libraries / wizard / wizard.php
vikappointments / site / helpers / libraries / wizard Last commit date
classes 3 days ago index.html 3 days ago step.php 3 days ago wizard.php 3 days ago
wizard.php
695 lines
1 <?php
2 /**
3 * @package VikAppointments
4 * @subpackage core
5 * @author E4J s.r.l.
6 * @copyright Copyright (C) 2021 E4J s.r.l. All Rights Reserved.
7 * @license http://www.gnu.org/licenses/gpl-2.0.html GNU/GPL
8 * @link https://vikwp.com
9 */
10
11 // No direct access
12 defined('ABSPATH') or die('No script kiddies please!');
13
14 VAPLoader::import('libraries.wizard.step');
15
16 /**
17 * Collection class used to manage the steps needed to complete
18 * a basic configuration of VikAppointments.
19 *
20 * @since 1.7.1
21 */
22 class VAPWizard implements ArrayAccess, IteratorAggregate
23 {
24 /**
25 * Singleton property.
26 *
27 * @var VAPWizard
28 */
29 protected static $instance = null;
30
31 /**
32 * A list of wizard steps.
33 *
34 * @var VAPWizardStep[]
35 */
36 protected $steps = array();
37
38 /**
39 * Flag used to check whether the wizard has been dismissed.
40 *
41 * @var boolean
42 */
43 protected $done;
44
45 /**
46 * Flag used to check whether the wizard has been set up.
47 *
48 * @var boolean
49 */
50 protected $setup = false;
51
52 /**
53 * An internal configuration setup.
54 *
55 * @var array
56 */
57 protected $config;
58
59 /**
60 * A list of include paths.
61 *
62 * @var array
63 */
64 protected $paths = array();
65
66 /**
67 * Returns the wizard singleton.
68 *
69 * @return VAPWizard
70 */
71 public static function getInstance()
72 {
73 if (static::$instance === null)
74 {
75 static::$instance = new static();
76 }
77
78 return static::$instance;
79 }
80
81 /**
82 * Class constructor.
83 * Cannot directly construct the class.
84 */
85 protected function __construct()
86 {
87 $config = VAPFactory::getConfig();
88
89 // check whether the wizard is still active
90 $this->done = $config->getBool('wizardstate', false);
91 // retrieve wizard config from database
92 $this->config = $config->getArray('wizardconfig', array());
93
94 // add default include path
95 $this->addIncludePath(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'classes');
96 }
97
98 /**
99 * Class cloner.
100 */
101 protected function __clone()
102 {
103 // cannot clone the class
104 }
105
106 /**
107 * Class destructor.
108 */
109 public function __destruct()
110 {
111 $config = VAPFactory::getConfig();
112
113 // iterate step
114 foreach ($this->steps as $step)
115 {
116 // register step options
117 $this->config[$step->getID()] = $step->getOptions();
118 }
119
120 // store wizard state into database
121 $config->set('wizardstate', (int) $this->done);
122 // store wizard config into database
123 $config->set('wizardconfig', $this->config);
124 }
125
126 /**
127 * Checks whether the wizard has been dismissed.
128 *
129 * @return boolean True if dismissed, false otherwise.
130 */
131 public function isDone()
132 {
133 return $this->done;
134 }
135
136 /**
137 * Marks the wizard as completed.
138 *
139 * @return self This object to support chaining.
140 */
141 public function done()
142 {
143 $this->done = true;
144
145 return $this;
146 }
147
148 /**
149 * Restores the wizard after completing it.
150 *
151 * @return self This object to support chaining.
152 */
153 public function restore()
154 {
155 $this->done = false;
156
157 // iterate step
158 foreach ($this->steps as $step)
159 {
160 // reset step configuration
161 $step->setOptions(array());
162 }
163
164 // reset config too
165 $this->config = array();
166
167 return $this;
168 }
169
170 /**
171 * Checks whether the wizard setup has been already invoked.
172 *
173 * @return boolean True if it is no more possible to setup the wizard.
174 */
175 public function isSetup()
176 {
177 return $this->setup;
178 }
179
180 /**
181 * Set up the wizard.
182 *
183 * @param array $steps A list of steps to auto-add.
184 *
185 * @return boolean False in case the setup was already made.
186 */
187 public function setup(array $steps = array())
188 {
189 if ($this->isSetup())
190 {
191 // cannot setup more than once
192 return false;
193 }
194
195 // flag setup to avoid entering here again
196 $this->setup = true;
197
198 // retrieve event dispatcher
199 $dispatcher = VAPFactory::getEventDispatcher();
200
201 /**
202 * Trigger event on wizard setup, useful to preload all the needed resources.
203 *
204 * @param VAPWizard $wizard The wizard instance.
205 *
206 * @return void
207 *
208 * @since 1.7.1
209 */
210 $dispatcher->trigger('onSetupVikAppointmentsWizard', array($this));
211
212 // iterate include paths
213 foreach ($this->getIncludePaths() as $path)
214 {
215 // check if we have a directory
216 if (is_dir($path))
217 {
218 // scan all files contained within the directory
219 $files = glob(rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . '*.php');
220 }
221 else if (is_file($path) && preg_match("/\.php$/", $path))
222 {
223 // take only the specified file
224 $files = array($path);
225 }
226 else
227 {
228 $files = array();
229 }
230
231 // iterate files one by one
232 foreach ($files as $file)
233 {
234 // require file only once
235 require_once $file;
236 }
237 }
238
239 $app = JFactory::getApplication();
240
241 // iterate the given steps
242 foreach ($steps as $step)
243 {
244 try
245 {
246 if (!$step instanceof VAPWizardStep)
247 {
248 // try to instantiate the step
249 $classname = preg_replace("/\.php$/", '', $step);
250 $classname = preg_replace("/[-_]+/", ' ', $classname);
251 $classname = preg_replace("/\s+/", '', ucfirst($step));
252 $classname = 'VAPWizardStep' . $classname;
253
254 // make sure the class exists
255 if (!class_exists($classname))
256 {
257 // throw error
258 throw new Exception(sprintf('Wizard step [%s] not found', $classname), 404);
259 }
260
261 // instantiate wizard step
262 $step = new $classname();
263 }
264
265 // try to add the step
266 $this->addStep($step);
267 }
268 catch (Exception $e)
269 {
270 // catch error, enqueue message and go ahead
271 $app->enqueueMessage($e->getMessage(), 'error');
272 }
273 }
274
275 /**
276 * Trigger event after completing the wizard setup.
277 * This is useful, in example, to rearrange the registered steps.
278 *
279 * @param VAPWizard $wizard The wizard instance.
280 *
281 * @return void
282 *
283 * @since 1.7.1
284 */
285 $dispatcher->trigger('onAfterSetupVikAppointmentsWizard', array($this));
286 }
287
288 /**
289 * Registers a new include path in which to search
290 * for the supported wizard steps.
291 *
292 * @param mixed $paths Either an array or the path to include.
293 *
294 * @return self This object to support chaining.
295 */
296 public function addIncludePath($paths)
297 {
298 $paths = (array) $paths;
299
300 // iterate paths
301 foreach ($paths as $path)
302 {
303 // make sure the paths hasn't been registered yet
304 if (!in_array($path, $this->paths))
305 {
306 $this->paths[] = $path;
307 }
308 }
309 }
310
311 /**
312 * Returns a list of include paths.
313 *
314 * @return array The include paths.
315 */
316 public function getIncludePaths()
317 {
318 return $this->paths;
319 }
320
321 /**
322 * Returns a list of steps that haven't been ignored.
323 *
324 * @return array
325 */
326 public function getActiveSteps()
327 {
328 $steps = array();
329
330 // iterate steps
331 foreach ($this->steps as $step)
332 {
333 // make sure the step is still active
334 if (!$step->isIgnored())
335 {
336 // push step within the list
337 $steps[] = $step;
338 }
339 }
340
341 return $steps;
342 }
343
344 /**
345 * Returns the number of registered steps.
346 *
347 * @return integer The steps count.
348 */
349 public function getStepsCount()
350 {
351 return count($this->steps);
352 }
353
354 /**
355 * Returns the step at the specified index.
356 *
357 * @param integer $index The index to access.
358 *
359 * @return mixed The step at the specified index if exists, null otherwise.
360 */
361 public function getStep($index)
362 {
363 // make sure the index is not out of bounds
364 if (preg_match("/^\d+$/", $index) && $index >= 0 && $index < $this->getStepsCount())
365 {
366 return $this->steps[$index];
367 }
368
369 return null;
370 }
371
372 /**
373 * Sets the step at the specified index.
374 *
375 * @param integer $index The index to access.
376 * @param VAPWizardStep $step The step to add.
377 *
378 * @return self This object to support chaining.
379 */
380 public function setStep($index, VAPWizardStep $step)
381 {
382 // make sure the index is not out of bounds (include the array limit for new items)
383 if (preg_match("/^\d+$/", $index) && $index >= 0 && $index <= $this->getStepsCount())
384 {
385 // replace previous step
386 $this->steps[$index] = $step;
387 }
388 }
389
390 /**
391 * Finds the index in which the specified step is stored.
392 *
393 * @param mixed $id Either the step id or the step itself.
394 *
395 * @return mixed The index of the step if exists, false otherwise.
396 */
397 public function indexOf($id)
398 {
399 // always use step ID to search
400 $id = $id instanceof VAPWizardStep ? $id->getID() : (string) $id;
401
402 foreach ($this->steps as $index => $step)
403 {
404 // compare IDs
405 if ($step->getID() == $id)
406 {
407 // step found, return current index
408 return $index;
409 }
410 }
411
412 // step not found
413 return false;
414 }
415
416 /**
417 * Adds a new step within the list.
418 *
419 * @param VAPWizardStep $step The step to add.
420 * @param mixed $index The index in which the step should be stored.
421 * If not specified, the step will be always
422 * pushed at the end of the list.
423 *
424 * @return self This object to support chaining.
425 */
426 public function addStep(VAPWizardStep $step, $index = null)
427 {
428 // check whether the step was already added
429 if ($this->indexOf($step) === false)
430 {
431 $id = $step->getID();
432
433 // fetch step options
434 $options = isset($this->config[$id]) ? $this->config[$id] : array();
435
436 // attach options to step
437 $step->setOptions($options);
438
439 if ($index === null || $index === false)
440 {
441 // push step at the end of the list
442 $this->steps[] = $step;
443 }
444 else
445 {
446 // insert step at the specified position
447 array_splice($this->steps, $index, 0, array($step));
448 }
449 }
450
451 return $this;
452 }
453
454 /**
455 * Adds a new step after the specified one.
456 *
457 * @param VAPWizardStep $step The step to add.
458 * @param mixed $id Either the step ID or the step itself.
459 *
460 * @return self This object to support chaining.
461 */
462 public function addStepAfter(VAPWizardStep $step, $id)
463 {
464 // find index in which the step is located
465 $index = $this->indexOf($id);
466
467 if ($index !== false)
468 {
469 // increase index by one to add the step
470 // one slot after
471 $index++;
472 }
473
474 // Add step at the specified position.
475 // In case the step doesn't exist, it will
476 // be added at the end of the queue.
477 return $this->addStep($step, $index);
478 }
479
480 /**
481 * Adds a new step before the specified one.
482 *
483 * @param VAPWizardStep $step The step to add.
484 * @param mixed $id Either the step ID or the step itself.
485 *
486 * @return self This object to support chaining.
487 */
488 public function addStepBefore(VAPWizardStep $step, $id)
489 {
490 // find index in which the step is located
491 $index = $this->indexOf($id);
492
493 // Add step at the specified position.
494 // In case the step doesn't exist, it will
495 // be added at the end of the queue.
496 return $this->addStep($step, $index);
497 }
498
499 /**
500 * Removes the specified step from the list.
501 *
502 * @param mixed $step Either the step ID, the step index or the step itself.
503 *
504 * @return boolean True if removed, false otherwise.
505 */
506 public function removeStep($step)
507 {
508 if (preg_match("/^\d+$/", $step))
509 {
510 // try to directly access the array as index
511 $step = $this->getStep($step);
512 }
513 else
514 {
515 // lets recover the index with the given argument
516 $step = $this->indexOf($step);
517 }
518
519 // make sure the step exists
520 if (!is_int($step))
521 {
522 // step not found
523 return false;
524 }
525
526 // splice array at the index found
527 $splice = array_splice($this->steps, $step, 1);
528
529 // extract removed step from list
530 $step = array_shift($splice);
531
532 if ($step)
533 {
534 // get step ID
535 $id = $step->getID();
536
537 // iterate registered steps
538 foreach ($this->steps as $tmp)
539 {
540 // try to detach dependency from removed step
541 $tmp->removeDependency($id);
542 }
543 }
544
545 // step removed
546 return true;
547 }
548
549 /**
550 * Checks whether all the steps of the wizard has been completed.
551 *
552 * @return boolean True if completed, false otherwise.
553 */
554 public function isCompleted()
555 {
556 // iterate active steps
557 foreach ($this->getActiveSteps() as $step)
558 {
559 // check whether the step has been completed
560 if (!$step->isCompleted())
561 {
562 // step not completed, return false
563 return false;
564 }
565 }
566
567 // all steps have been completed
568 return true;
569 }
570
571 /**
572 * Calculates the overall progress of the wizard, based
573 * on the active steps.
574 *
575 * @return integer The percentage progress.
576 */
577 public function getProgress()
578 {
579 // get all active steps
580 $steps = $this->getActiveSteps();
581 $total = 0;
582
583 if (!$steps)
584 {
585 // return 100% completion in case of no active steps
586 return 100;
587 }
588
589 // iterate steps
590 foreach ($steps as $step)
591 {
592 // increase progress total
593 $total += $step->getProgress();
594 }
595
596 // calculate progress AVG
597 return round($total / count($steps));
598 }
599
600 /**
601 * Checks if the given item exists.
602 *
603 * @param mixed $key Either the step ID or an index.
604 *
605 * @return boolean True if exists, false otherwise.
606 */
607 #[ReturnTypeWillChange]
608 public function offsetExists($key)
609 {
610 if (preg_match("/^\d+$/", $key))
611 {
612 // try to directly access the array as index
613 return $this->getStep($key) !== null;
614 }
615
616 // lets recover the index with the given argument
617 return $this->indexOf($key) !== false;
618 }
619
620 /**
621 * Returns the value for the specified item.
622 *
623 * @param mixed $key Either the step ID or an index.
624 *
625 * @return mixed The item value.
626 */
627 #[ReturnTypeWillChange]
628 public function offsetGet($key)
629 {
630 if (!preg_match("/^\d+$/", $key))
631 {
632 // route step ID to index
633 $key = $this->indexOf($key);
634 }
635
636 // return the step at the specified index
637 return $this->getStep($key);
638 }
639
640 /**
641 * Sets the given item.
642 *
643 * @param mixed $key Either the step ID or an index.
644 * @param VAPWizardStep $value The step to add.
645 *
646 * @return void
647 */
648 #[ReturnTypeWillChange]
649 public function offsetSet($key, $value)
650 {
651 if ($key === null)
652 {
653 // append step at the end of the array
654 $this->addStep($value);
655 }
656 else
657 {
658 if (!preg_match("/^\d+$/", $key))
659 {
660 // route step ID to index
661 $key = $this->indexOf($key);
662 }
663
664 // adds the step at the specified position
665 $this->setStep($key, $value);
666 }
667 }
668
669 /**
670 * Removes the given item.
671 *
672 * @param mixed $key Either the step ID or an index.
673 *
674 * @return void
675 */
676 #[ReturnTypeWillChange]
677 public function offsetUnset($key)
678 {
679 // remove step from the list
680 $this->removeStep($key);
681 }
682
683 /**
684 * Implements an iterator for the registered steps.
685 *
686 * @return ArrayIterator
687 */
688 #[ReturnTypeWillChange]
689 public function getIterator()
690 {
691 // return an iterator for the active steps
692 return new ArrayIterator($this->steps);
693 }
694 }
695