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 / customfields / loader.php
vikappointments / site / helpers / libraries / customfields Last commit date
rules 3 days ago types 3 days ago control.php 3 days ago emptyloader.php 3 days ago factory.php 3 days ago field.php 3 days ago index.html 3 days ago loader.php 3 days ago renderer.php 3 days ago requestor.php 3 days ago rule.php 3 days ago
loader.php
776 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 /**
15 * VikAppointments custom fields loader class.
16 * Usage example:
17 *
18 * $fields = VAPCustomFieldsLoader::getInstance()
19 * ->customers()
20 * ->ofEmployee($id_employee)
21 * ->forService($id_services)
22 * ->excludeSeparator()
23 * ->translate()
24 * ->fetch();
25 *
26 * @since 1.7
27 */
28 class VAPCustomFieldsLoader
29 {
30 /**
31 * Cache of query results.
32 *
33 * @var array
34 */
35 protected static $results = array();
36
37 /**
38 * The default country code in case it is not specified.
39 *
40 * @var string
41 */
42 public static $defaultCountry = 'US';
43
44 /**
45 * Holds the query builder to fetch the custom fields.
46 *
47 * @var mixed
48 */
49 protected $query;
50
51 /**
52 * The group to which the custom fields should belong.
53 *
54 * @var integer
55 */
56 protected $group = 0;
57
58 /**
59 * When specified, only the custom fields belonging
60 * to the specified employee will be loaded.
61 *
62 * @var integer
63 */
64 protected $employee = 0;
65
66 /**
67 * When specified, only the custom fields belonging
68 * to the specified services will be loaded.
69 *
70 * @var array
71 */
72 protected $services = array();
73
74 /**
75 * Filter used to take only the custom fields assigned
76 * to the specified language tag.
77 *
78 * @var string
79 */
80 protected $locale = '*';
81
82 /**
83 * Mask used to filter the custom fields.
84 *
85 * @var integer
86 */
87 protected $filterMask = 0;
88
89 /**
90 * Flag used to translate the records after loading them.
91 * It is possible to specify the language to use for translations.
92 *
93 * @var mixed
94 */
95 protected $translate = false;
96
97 /**
98 * The page where the field should be displayed.
99 *
100 * @var string
101 * @since 1.7.7
102 */
103 protected $page = '*';
104
105 /**
106 * Class constructor proxy for immediate chaining.
107 *
108 * @return self
109 */
110 public static function getInstance()
111 {
112 return new static();
113 }
114
115 /**
116 * Class constructor.
117 */
118 public function __construct()
119 {
120 $this->init();
121 }
122
123 /**
124 * Loads the custom fields for the customers.
125 *
126 * @return self This object to support chaining.
127 */
128 public function customers()
129 {
130 $this->group = static::CUSTOMERS;
131
132 return $this;
133 }
134
135 /**
136 * Loads the custom fields for the employees.
137 *
138 * @return self This object to support chaining.
139 */
140 public function employees()
141 {
142 $this->group = static::EMPLOYEES;
143
144 return $this;
145 }
146
147 /**
148 * Loads the custom fields of the specified employee.
149 * When this method is called, it forces the group to
150 * be "customers", since the custom fields for the
151 * employees do not support this assignment.
152 *
153 * @param integer $id The employee ID.
154 *
155 * @return self This object to support chaining.
156 */
157 public function ofEmployee($id)
158 {
159 // force "customers" group
160 $this->customers();
161
162 $this->employee = max(array(0, (int) $id));
163
164 return $this;
165 }
166
167 /**
168 * Loads the custom fields for the specified service(s).
169 * When this method is called, it forces the group to
170 * be "customers", since the custom fields for the
171 * employees do not support this assignment.
172 *
173 * @param mixed $ids Either a service ID or an array.
174 *
175 * @return self This object to support chaining.
176 */
177 public function forService($ids)
178 {
179 // force "customers" group
180 $this->customers();
181
182 foreach ((array) $ids as $id)
183 {
184 // sanitize service ID
185 $id = max(array(0, (int) $id));
186
187 // add service only once
188 if ($id && !in_array($id, $this->services))
189 {
190 $this->services[] = $id;
191 }
192 }
193
194 return $this;
195 }
196
197 /**
198 * Sets the language filter.
199 *
200 * @param string $lang The language tag to set. Use false,
201 * null or '*' to ignore this filter.
202 *
203 * @return self This object to support chaining.
204 */
205 public function setLanguageFilter($lang = 'auto')
206 {
207 if (strcasecmp($lang, 'auto') == 0 || $lang === '')
208 {
209 // use the current language tag
210 $this->locale = JFactory::getLanguage()->getTag();
211 }
212 else
213 {
214 // use the specified language tag
215 $this->locale = $lang;
216 }
217
218 return $this;
219 }
220
221 /**
222 * Exclude the required checkboxes from the query.
223 *
224 * @return self This object to support chaining.
225 */
226 public function noRequiredCheckbox()
227 {
228 if (!$this->filterMask)
229 {
230 // create mask from scratch
231 $this->filterMask = static::EXCLUDE_REQUIRED_CHECKBOX;
232 }
233 else
234 {
235 // extend existing mask with new value
236 $this->filterMask |= static::EXCLUDE_REQUIRED_CHECKBOX;
237 }
238
239 return $this;
240 }
241
242 /**
243 * Exclude the separators from the query.
244 *
245 * @return self This object to support chaining.
246 */
247 public function noSeparator()
248 {
249 if (!$this->filterMask)
250 {
251 // create mask from scratch
252 $this->filterMask = static::EXCLUDE_SEPARATOR;
253 }
254 else
255 {
256 // extend existing mask with new value
257 $this->filterMask |= static::EXCLUDE_SEPARATOR;
258 }
259
260 return $this;
261 }
262
263 /**
264 * Exclude the input files from the query.
265 *
266 * @return self This object to support chaining.
267 */
268 public function noInputFile()
269 {
270 if (!$this->filterMask)
271 {
272 // create mask from scratch
273 $this->filterMask = static::EXCLUDE_FILE;
274 }
275 else
276 {
277 // extend existing mask with new value
278 $this->filterMask |= static::EXCLUDE_FILE;
279 }
280
281 return $this;
282 }
283
284 /**
285 * Sets the page where the custom fields should be displayed.
286 *
287 * @param string $page The target page. Use * to ignore.
288 *
289 * @return self This object to support chaining.
290 *
291 * @since 1.7.7
292 */
293 public function onPage($page)
294 {
295 $this->page = $page;
296
297 return $this;
298 }
299
300 /**
301 * Sets whether the fields should be translated or not.
302 *
303 * @param mixed Either a boolean or a language tag.
304 *
305 * @return self This object to support chaining.
306 */
307 public function translate($flag = true)
308 {
309 $this->translate = $flag;
310
311 if ($this->translate && is_string($this->translate))
312 {
313 // auto-set language filter
314 $this->setLanguageFilter($this->translate);
315 }
316
317 return $this;
318 }
319
320 /**
321 * Finally executes the query and returns the matching fields.
322 *
323 * @return array An array of custom fields.
324 */
325 public function fetch()
326 {
327 $fields = array();
328
329 // prepare query options
330 $options = array(
331 'group' => $this->group,
332 'employee' => $this->employee,
333 'services' => $this->services,
334 'filter' => $this->filterMask,
335 );
336
337 // create signature
338 $sign = serialize(array_values($options));
339
340 // check whether the same query has been already executed
341 if (!isset(static::$results[$sign]))
342 {
343 static::$results[$sign] = array();
344
345 $dispatcher = VAPFactory::getEventDispatcher();
346
347 $dbo = JFactory::getDbo();
348
349 if (!$this->query)
350 {
351 // init query from scratch
352 $this->init();
353 }
354
355 // filter custom fields by group
356 $this->query->where($dbo->qn('c.group') . ' = ' . $this->group);
357
358 // check whether we are looking for the fields of the customers
359 if ($this->group == static::CUSTOMERS)
360 {
361 // filter by employee
362 $employee_where = array();
363 $employee_where[] = $dbo->qn('c.id_employee') . ' <= 0';
364
365 if ($this->employee)
366 {
367 $employee_where[] = $dbo->qn('c.id_employee') . ' = ' . $this->employee;
368
369 // extends with OR
370 $this->query->andWhere($employee_where, 'OR');
371 }
372 else
373 {
374 $this->query->where($employee_where);
375 }
376
377 // build query to count services
378 $countServices = $dbo->getQuery(true)
379 ->select('COUNT(1)')
380 ->from($dbo->qn('#__vikappointments_cf_service_assoc', 'a2'))
381 ->where($dbo->qn('a2.id_field') . ' = ' . $dbo->qn('c.id'));
382
383 // filter by services
384 if ($this->services)
385 {
386 // loads custom fields without assignments or that belong to the specified services
387 $this->query->leftjoin($dbo->qn('#__vikappointments_cf_service_assoc', 'a') . ' ON ' . $dbo->qn('a.id_field') . ' = ' . $dbo->qn('c.id'));
388 $this->query->andWhere(array(
389 $dbo->qn('a.id_service') . ' IN (' . implode(', ', $this->services) . ')',
390 '(' . $countServices . ') = 0',
391 ), 'OR');
392 }
393 else
394 {
395 // otherwise exclude the fields assigned to specific services
396 $this->query->where('(' . $countServices . ') = 0');
397 }
398 }
399
400 // exclude required checkboxes
401 if ($this->filterMask & static::EXCLUDE_REQUIRED_CHECKBOX)
402 {
403 $this->query->andWhere(array(
404 $dbo->qn('c.type') . ' <> ' . $dbo->q('checkbox'),
405 $dbo->qn('c.required') . ' = 0',
406 ));
407 }
408
409 // exclude separators
410 if ($this->filterMask & static::EXCLUDE_SEPARATOR)
411 {
412 $this->query->where($dbo->qn('c.type') . ' <> ' . $dbo->q('separator'));
413 }
414
415 // exclude input file
416 if ($this->filterMask & static::EXCLUDE_FILE)
417 {
418 $this->query->where($dbo->qn('c.type') . ' <> ' . $dbo->q('file'));
419 }
420
421 /**
422 * Filter the custom fields by language tag.
423 *
424 * @since 1.7
425 */
426 if ($this->locale && $this->locale != '*')
427 {
428 $this->query->andWhere(array(
429 $dbo->qn('c.locale') . ' = ' . $dbo->q($this->locale),
430 $dbo->qn('c.locale') . ' = ' . $dbo->q('*'),
431 $dbo->qn('c.locale') . ' = ' . $dbo->q(''),
432 ), 'OR');
433 }
434
435 /**
436 * Trigger hook to allow external plugins to manipulate the query used
437 * to load the custom fields through this helper class.
438 *
439 * @param mixed &$query A query builder object.
440 * @param array $options An array of query options.
441 *
442 * @return void
443 *
444 * @since 1.7
445 */
446 $dispatcher->trigger('onBeforeQueryCustomFields', array(&$this->query, $options));
447
448 $dbo->setQuery($this->query);
449 $rows = $dbo->loadAssocList();
450
451 /**
452 * Trigger hook to allow external plugins to manipulate the list of
453 * supported custom fields through this helper class.
454 *
455 * @param array &$rows An array of custom fields.
456 * @param array $options An array of query options.
457 *
458 * @return void
459 *
460 * @since 1.7
461 */
462 $dispatcher->trigger('onBeforeRegisterCustomFields', array(&$rows, $options));
463
464 // cache results
465 static::$results[$sign] = $rows;
466 }
467
468 // copy result in a local variable to apply the translation
469 $fields = static::$results[$sign];
470
471 if ($this->translate)
472 {
473 // translate the fields
474 static::doTranslate($fields, $this->translate);
475 }
476
477 /**
478 * Takes only the fields that can be actually displayed on the selected page.
479 *
480 * @since 1.7.7
481 */
482 $fields = array_values(array_filter($fields, function($field) {
483 // make sure the selected page is supported by the field
484 if ($this->page !== '*' && !in_array($field['page'], ['*', $this->page]))
485 {
486 return false;
487 }
488
489 return true;
490 }));
491
492 // reset instance to support new queries
493 $this->reset();
494
495 return $fields;
496 }
497
498 /**
499 * Initialize the class to support a query.
500 *
501 * @return self This object to support chaining.
502 */
503 protected function init()
504 {
505 $dbo = JFactory::getDbo();
506
507 $this->query = $dbo->getQuery(true);
508
509 // query to load all the supported columns
510 $columns = $dbo->getTableColumns('#__vikappointments_custfields');
511
512 // select all columns from custom fields table
513 foreach ($columns as $field => $type)
514 {
515 $this->query->select($dbo->qn('c.' . $field));
516 }
517
518 $this->query->from($dbo->qn('#__vikappointments_custfields', 'c'));
519 $this->query->where(1);
520
521 // group records since the query might use aggregators
522 $this->query->group($dbo->qn('c.id'));
523 // always sort fields by ascending ordering
524 $this->query->order($dbo->qn('c.ordering') . ' ASC');
525
526 return $this;
527 }
528
529 /**
530 * Resets the query.
531 *
532 * @return self This object to support chaining.
533 */
534 public function reset()
535 {
536 $this->query = null;
537 $this->group = 0;
538 $this->employee = 0;
539 $this->services = array();
540 $this->filterMask = 0;
541 $this->translate = false;
542 $this->page = '*';
543
544 return $this;
545 }
546
547 /**
548 * Translates the specified custom fields.
549 * The translation of the name will be placed in a different column 'langname'.
550 * The original 'name' column won't be altered.
551 *
552 * @param array $fields The records to translate.
553 * @param string $tag The locale in which the fields will be translated.
554 * Leave empty to use the current user locale.
555 *
556 * @return void
557 */
558 public static function doTranslate(array &$fields, $tag = null)
559 {
560 $ids = array();
561
562 /**
563 * Added support for missing fields in case the multilingual
564 * feature is disabled.
565 *
566 * @since 1.6.1
567 */
568 foreach ($fields as $i => $f)
569 {
570 if (!isset($f['_choose']))
571 {
572 // keep original 'choose' for select
573 $fields[$i]['_choose'] = $f['choose'];
574 }
575
576 if (!isset($f['langname']))
577 {
578 // backward compatibility for old translation technique
579 $fields[$i]['langname'] = JText::translate($f['name']);
580 }
581
582 $ids[] = $fields[$i]['id'];
583 }
584
585 // do not proceed in case multi-lingual feature is turned off
586 if (!VikAppointments::isMultilanguage() || !count($fields))
587 {
588 return;
589 }
590
591 // auto-detect language in case it is missing
592 if (empty($tag) || !is_string($tag))
593 {
594 $tag = JFactory::getLanguage()->getTag();
595 }
596
597 // get translator
598 $translator = VAPFactory::getTranslator();
599
600 // pre-load fields translations
601 $fieldsLang = $translator->load('custfield', array_unique($ids), $tag);
602
603 // apply translations
604 foreach ($fields as &$field)
605 {
606 // get custom field translation
607 $tx = $fieldsLang->getTranslation($field['id'], $tag);
608
609 if ($tx)
610 {
611 // apply translations
612 $field['langname'] = JText::translate($tx->name);
613 $field['description'] = $tx->description;
614 $field['poplink'] = $tx->poplink;
615 $field['choose'] = $tx->choose;
616 }
617 }
618 }
619
620 /**
621 * Translates the specified custom fields array data.
622 *
623 * @param array $data The associative array with the CF data.
624 * @param array $fields The custom fields (MUST BE already translated).
625 * @param mixed $langtag The language tag to use.
626 *
627 * @return array The translated CF data array.
628 */
629 public static function translateObject($data, array $fields, $langtag = null)
630 {
631 $tmp = array();
632
633 if ($langtag)
634 {
635 // reload system language
636 VikAppointments::loadLanguage($langtag);
637 }
638
639 // import field class
640 VAPLoader::import('libraries.customfields.field');
641
642 foreach ($fields as $cf)
643 {
644 $k = $cf['name'];
645
646 if (!array_key_exists($k, $data))
647 {
648 // field not found inside the given object, go to next one
649 continue;
650 }
651
652 // create field instance
653 $field = VAPCustomField::getInstance($cf);
654
655 // inject specified language tag
656 $field->set('langtag', $langtag);
657
658 // get a more readable text of the saved value
659 $tmp[$k] = $field->getReadableValue($data[$k]);
660 }
661
662 return $tmp;
663 }
664
665 /**
666 * Return the default country code assigned to the phone number custom field.
667 *
668 * @param string $langtag The langtag to retrieve the proper country depending
669 * on the current language.
670 * @param mixed $default The default return value in case of missing field.
671 *
672 * @return string The default country code.
673 */
674 public static function getDefaultCountryCode($langtag = null, $default = true)
675 {
676 /**
677 * Auto-detect language tag if not specified.
678 *
679 * @since 1.6.3
680 */
681 if (!$langtag)
682 {
683 $langtag = JFactory::getLanguage()->getTag();
684 }
685
686 $dbo = JFactory::getDbo();
687
688 $q = $dbo->getQuery(true);
689
690 $q->select(array(
691 $dbo->qn('c.id'),
692 $dbo->qn('c.choose'),
693 $dbo->qn('l.choose', 'lang_choose'),
694 $dbo->qn('l.tag'),
695 ))
696 ->from($dbo->qn('#__vikappointments_custfields', 'c'))
697 ->leftjoin($dbo->qn('#__vikappointments_lang_customf', 'l')
698 . ' ON ' . $dbo->qn('l.id_customf') . ' = ' . $dbo->qn('c.id')
699 . ' AND ' . $dbo->qn('l.tag') . ' = ' . $dbo->q($langtag))
700 ->where($dbo->qn('c.rule') . ' = ' . $dbo->q('phone'));
701
702 $dbo->setQuery($q, 0, 1);
703 $row = $dbo->loadAssoc();
704
705 if (!$row)
706 {
707 /**
708 * Evaluate to return default country code or specified value.
709 *
710 * @since 1.6.3
711 */
712 return $default === true ? self::$defaultCountry : $default;
713 }
714
715 // make sure we found a matching custom field
716 if ($row['tag'] == $langtag && strlen($row['lang_choose']))
717 {
718 // use country code defined in langtag
719 $row['choose'] = $row['lang_choose'];
720 }
721 // check if we should return the specified default value
722 else if ($default !== true)
723 {
724 // unset string to return default value
725 $row['choose'] = '';
726 }
727
728 $default = $default === true ? self::$defaultCountry : $default;
729
730 // if we have a valid country code, return it, otherwise return the default value
731 return strlen($row['choose']) ? $row['choose'] : $default;
732 }
733
734 /**
735 * Customers identifier group.
736 *
737 * @var integer
738 */
739 const CUSTOMERS = 0;
740
741 /**
742 * Employees identifier group.
743 *
744 * @var integer
745 */
746 const EMPLOYEES = 1;
747
748 /**
749 * No filter mask constant.
750 *
751 * @var integer
752 */
753 const NO_FILTER = 0;
754
755 /**
756 * Exclude required checkbox filter mask constant.
757 *
758 * @var integer
759 */
760 const EXCLUDE_REQUIRED_CHECKBOX = 1;
761
762 /**
763 * Exclude separator filter mask constant.
764 *
765 * @var integer
766 */
767 const EXCLUDE_SEPARATOR = 2;
768
769 /**
770 * Exclude input file mask constant.
771 *
772 * @var integer
773 */
774 const EXCLUDE_FILE = 4;
775 }
776