The Framework Code

class/framework/model/form.php

File List

<?php
/**
 * A model class for the RedBean object Form
 *
 * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! This is a Framework system class - do not edit !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
 *
 * @author Lindsay Marshall <lindsay.marshall@ncl.ac.uk>
 * @copyright 2016-2021 Newcastle University
 * @package Framework\Model
 */
    namespace Framework\Model;

    use \Config\Config;
    use \Config\Framework as FW;
    use \Support\Context;
/**
 * A class implementing a RedBean model for Form beans
 * @psalm-suppress UnusedClass
 */
    final class Form extends \RedBeanPHP\SimpleModel
    {
/**
 * @var array<string> METHOD options for forms
 */
        private static array $methods     = ['', 'GET', 'POST'];
/**
 * @var array<string> Attributes for inputs
 * @phpcsSuppress SlevomatCodingStandard.Classes.UnusedPrivateElements
 */
        private static array $attributes  = ['type', 'class', 'name', 'placeholder'];
/**
 * @var array<array<bool>>   Key is name of field and the array contains flags for checks
 * @phpcsSuppress SlevomatCodingStandard.Classes.UnusedPrivateElements
 */
        private static array $editfields = [
            'name'            => [TRUE, FALSE],         // [NOTEMPTY, CHECK/RADIO]
            'action'          => [TRUE, FALSE],
            'method'          => [TRUE, FALSE],
            'idval'           => [FALSE, FALSE],
            'class'           => [FALSE, FALSE],
            'multipart'       => [FALSE, TRUE],
        ];
/**
 * @var array The kinds of flags that fields can have
 */
        private static array $flags = [
            'checked'       => ['Checked', TRUE, 0x01],
            'disabled'      => ['Disabled', FALSE, 0x02],
            'multiple'      => ['Multiple', TRUE, 0x04],
            'readonly'      => ['Readonly', FALSE, 0x08],
            'required'      => ['Required', FALSE, 0x10],
        ];
/**
 * @var bool flag to indicate inside optgroup - nested optgroups are NOT supported (at the moment)
 */
        private bool $optgroup = FALSE;

        use \ModelExtend\FWEdit;
        use \ModelExtend\MakeGuard;
/**
 * Return the form name
 */
        public function name() : string
        {
            return $this->bean->name;
        }
/**
 * Return the form's method
 */
        public function method() : string
        {
            return $this->bean->method;
        }
/**
 * Return the form's fields
 */
        public function fields() : array
        {
            return $this->bean->with('order by seqn,name')->ownFormField;
        }
/**
 * Return the form's fields by sequence
 *
 * Some fields deliberately share sequence numbers (e.g. checkboxes in a row)
 */
        public function sequence() : array
        {
            $res = [];
            foreach ($this->fields() as $fld)
            {
                $sqn = \explode('/', $fld->seqn);
                if (\count($sqn) > 1)
                { // there are sub orderings in here
                    $res[$sqn[0]][$sqn[1]] = $fld;
                }
                else
                {
                    $res[$sqn[0]][] = $fld;
                }
            }
            return $res;
        }
/**
 * Resequence the fields so that they are all multiples of 10
 *
 * Remember that some items deliberatley share sequence numbers!
 *
 * @todo support resequencing of sub-orderings
 */
        public function resequence() : void
        {
            $seqn = 10;
            foreach ($this->sequence() as $flds)
            {
                foreach ($flds as $fld)
                {
                    $sqn = \explode('/', $fld->seqn);
                    $sqn[0] = $seqn;
                    $fld->seqn = \implode('/', $sqn);
                    \R::store($fld);
                }
                $seqn += 10;
            }
        }
/**
 * Setup for an edit
 *
 * @see Framework\Pages\Admin
 * @phpcsSuppress SlevomatCodingStandard.Functions.UnusedParameter
 */
        public function startEdit(Context $context, array $rest) : void
        {
            $context->local()->addVal('flags', self::$flags);
        }
/**
 * Handle a form edit
 *
 * @see Framework\Pages\Admin
 */
        public function edit(Context $context) : array
        {
            $fdt = $context->formData('post');
            $emess = $this->doFields($fdt);

            foreach (array_keys($fdt->fetchArray('new')) as $ix)
            {
                if (($type = $fdt->fetch(['type', $ix], '')) !== '')
                {
                    $fld = \R::dispense(FW::FORMFIELD);
                    $fld->type = $type;
                    foreach (['label', 'name', 'class', 'idval', 'placeholder', 'value', 'other', 'seqn'] as $fname)
                    {
                        $fld->$fname = $fdt->fetch(['fld'.$fname, $ix], '');
                    }
                    $fld->flags = 0;
                    foreach (array_keys(self::$flags) as $fn)
                    {
                        $fld->flags |= $fdt->fetch(['fld'.$fn, $ix], 0);
                    }
                    \R::store($fld);
                    $this->bean->xownFormList[] = $fld;
                }
            }
            \R::store($this->bean);
            return [!empty($emess), $emess];
        }
/**
 * View a form
 *
 * @psalm-suppress PossiblyUnusedParameter
 * @phpcsSuppress SlevomatCodingStandard.Functions.UnusedParameter
 */
        public function view(Context $context, array $rest) : void
        {
        }
/**
 * Render a form
 *
 * @param $values Values to enter into form
 * @param $noform If TRUE then do not put out the <form> and </form> tags - useful when building forms in parts
 */
        public function render(array $values = [], bool $noform = FALSE) : string
        {
            if (!$noform || $this->bean->method == 0)
            {
                $form = '<form action="'.
                    ($this->bean->action === '' ? '#' : $this->bean->action).'" '.
                    ($this->bean->class !== '' ? (' class="'.$this->bean->class.'"') : '').
                    ($this->bean->idval !== '' ? (' id="'.$this->bean->idval.'"') : '').
                    ' method="'.self::$methods[$this->bean->method].'"'.
                    ($this->bean->multipart ? ' enctype="multipart/form-data"' : '').
                    ' role="form">'.PHP_EOL;
            }
            else
            {
                $form = '';
            }
            $fset = FALSE;
            foreach ($this->sequence() as $flds)
            {
                $fld = \reset($flds);
                $crlabel = '';
                switch ($fld->type)
                {
                case 'fieldset':
                    $form .= ($fset ? '</fieldset>' : '').'<fieldset'.$fld->fieldAttr('', TRUE).'>'.($fld->label !== '' ? '<legend>'.$fld->label.'</legend>' : '');
                    $fset = TRUE;
                    break;

                case 'endfset':
                    if ($fset)
                    {
                        $form .= '</fieldset>';
                        $fset = FALSE;
                    }
                    break;

                case 'label': // labelling for checkbox and radio groupings
                    $crlabel = '<label'.$fld->fieldAttr('', FALSE).'>'.$fld->label.'</label>'; // make the label
                    \array_shift($flds); // pop off the label- the rest will be checkboxes or radios
                    /* ******* DROP THROUGH ******* */
                case 'checkbox':
                case 'radio':
                    $form .= '<div class="form-group">'.$crlabel.'<div class="form-check form-check-inline">';
                    foreach ($flds as $fld)
                    {
                        if (isset($values[$fld->name]) && $fld->value == $values[$fld->name])
                        {
                            $fld->checked = 1;
                        }
                        $input = '<input'.$fld->fieldAttr('', TRUE).'/> ';
                        $form .= $fld->doLabel(FALSE, 'form-check-label mr-2', $input); // need to do this first as it might set the label field in $fld
                    }
                    $form .= '</div></div>';
                    break;

                case 'select':
                    $form .= '<div class="form-group">'.$fld->doLabel(TRUE).'<select'.$fld->fieldAttr('form-control', FALSE).'>';
                    $this->optgroup = FALSE;
                    foreach ($values[$fld->name] as $option)
                    {
                        $form .= $this->doOption($option);
                    }
                    /** @psalm-suppress TypeDoesNotContainType */
                    if ($this->optgroup)
                    { // close any open optgroup
                        $form .= '</optgroup>';
                    }
                    $form .= '</select></div>';
                    break;

                case 'textarea':
                    $form .= '<div class="form-group">'.$fld->doLabel(TRUE).'<textarea'.$fld->fieldAttr('form-control', FALSE).'>'.($values[$fld->name] ?? $fld->value).'</textarea></div>';
                    break;

                case 'recaptcha':
                    /** @psalm-suppress UndefinedConstant */
                    if (Config::RECAPTCHA != 0)
                    {
                        $form .= '<div class="form-group"><button '.$fld->fieldAttr('', FALSE).' data-sitekey="'.Config::RECAPTCHAKEY.'">'.$fld->value.'</button>';
                        break;
                    }
                    /* ******* DROP THROUGH ******* */
                case 'submit':
                case 'button':
                    $form .= '<div class="form-group"><button'.$fld->fieldAttr('', FALSE).'>'.$fld->value.'</button></div>';
                    break;

                default: // all the other types are very much the same.
                    if (isset($values[$fld->name]))
                    {
                        $fld->value = $values[$fld->name];
                    }
                    $form .= '<div class="form-group">'.$fld->doLabel(TRUE).'<input'.$fld->fieldAttr('form-control', TRUE, $values).'/></div>';
                    break;
                }
                $form .= \PHP_EOL;
            }
            if ($fset)
            {
                $form .= '</fieldset>';
            }
            return $noform || $this->bean->method == 0 ? $form : $form.'</form>';
        }
/**
 * Deal with an optgroup possibility
 */
        private function optgroup(bool $mkit, ?string $label, bool $disabled) : string
        {
            $selbod = $this->optgroup ? '</optgroup>' : ''; // if one open already, close it
            $this->optgroup = $mkit;
            assert(!$mkit || !is_null($label));
            return $selbod.($mkit ? '<optgroup label="'.$label.'"'.($disabled ? ' disabled="disabled"' : '').'>' : '');
        }
/**
 * Handle making an option. Deals with optgroups
 */
        private function doOption(array|object $option) : string
        {
            $form = '';
            if (\is_object($option))
            {
                if (isset($option->optgroup))
                {
                    if ($this->optgroup)
                    { // one open already so close it
                        $form = '</optgroup>';
                    }
                    if ($option->optgroup !== '') // If the name is not empty then start a new one
                    {
                        $this->optgroup = TRUE;
                        return $form.'<optgroup label="'.$option->optgroup.'"'.(isset($option->disabled) ? ' disabled="disabled"' : '').'>';
                    }
                    // $og = $this->optgroup($option->optgroup !== '', $option->optgroup, $option->disabled;
                }
                return $this->mkoption($option->value, $option->text, isset($option->selected), isset($option->disabled));
            }
            assert(\is_array($option)); // $options must be an array if we get here
            if ($option[0] === NULL)
            {
                if ($this->optgroup)
                { // one open already so close it
                    $form = '</optgroup>';
                }
                if ($option[1] !== NULL) // If the name is also NULL then we want to close an open optgroup without starting a new one
                {
                    $this->optgroup = TRUE;
                    return $form.'<optgroup label="'.$option[1].'"'.(isset($option[2]) ? ' disabled="disabled"' : '').'>';
                }
                // $og = $this->optgroup(!is_null($option[1]), $option[1], $option[2]);
            }
            return $this->mkoption($option[0], $option[1], isset($option[2]), isset($option[3]));
        }
/**
 * Make an option tag
 */
        private function mkOption(string $value, string $text, bool $selected, bool $disabled) : string
        {
            return '<option value="'.$value.'"'.($disabled ? ' disabled="disabled"' : '').($selected ? ' selected="selected"' : '').'>'.$text.'</option>';
        }
/**
 * Add a new form, called when adding a new form via ajax
 *
 * @see Framework\Ajax::bean
 */
        public static function add(Context $context) : \RedBeanPHP\OODBBean
        {
            $fdt = $context->formdata('post');
            $p = \R::dispense(FW::FORM);
            foreach (['name', 'action', 'class', 'idval', 'method'] as $fld)
            {
                $p->{$fld} = $fdt->mustFetch($fld);
            }
            $p->multipart = $fdt->fetch('multipart', 0);
            \R::store($p);
            return $p;
        }
    }
?>