首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >HTML表单类

HTML表单类
EN

Code Review用户
提问于 2023-05-03 17:12:19
回答 1查看 81关注 0票数 2

在我之前的问题之后 I将代码修改为下面的代码。我正在使用PHP 7.0.33,并试图尽可能多地完成PSR-12。在docblock中的“浓缩用法”中,您可以找到一个完整的复制粘贴示例。

以下是代码:

代码语言:javascript
复制
/**
 * This class aims to render a valid HTML form block
 *
 * @author julio
 * @date 1/5/2023
 *
 *
 * -- Condensed usage --
 *
$form = new Form([
    'name'=>"my_form",
    'action'=>"destination.php",
    'method'=>"post"
]);

$form->add_tag("fieldset");
$form->add_tag("legend");
$form->add_tag_attributes(['text'=>"My FLDST 1"]);

$form->add_tag("label");
$form->add_tag_attributes([
    'for'=>"my_text",
    'text'=>"My Text:"
]);
$form->add_tag("input");
$form->add_tag_attributes([
    'id'=>"my_text",
    'type'=>"text",
    'name'=>"my_text",
    'value'=>"", 'required'=>null
]);
$form->add_tag("label");
$form->add_tag_attributes([
    'for'=>"my_textarea",
    'text'=>"My Text Area:"
]);
$form->add_tag("textarea");
$form->add_tag_attributes([
    'id'=>"my_textarea",
    'name'=>"my_textarea",
    'cols'=>80,
    'rows'=>20,
    'text'=>"My textarea text"
]);

$form->add_tag("fieldset");

$form->add_tag("label");
$form->add_tag_attributes([
    'for'=>"my_check",
    'text'=>"My Check:"
]);
$form->add_tag("input");
$form->add_tag_attributes([
    'type'=>"checkbox",
    'name'=>"my_check",
    'value'=>"",
    'id'=>"my_check",
    'checked'=>true
]);

$form->add_tag("select");
$form->add_tag_attributes(['name'=>"my_select"]);
$form->add_tag_options(
    [
        "value11"=>"text11",
        "value12"=>"text12",
        "value13"=>"text13"
    ],
    ["value13"]
);
$form->add_tag("select");
$form->add_tag_attributes([
    'name'=>"my_other_select",
    'multiple'=>true
]);
$form->add_tag_options(
    [
        "value1"=> ["value11"=>"text11", "value12"=>"text12", "value13"=>"text13"],
        "value2"=> ["value21"=>"text21", "value22"=>"text22", "value23"=>"text23"],
        "value3"=> ["value31"=>"text31", "value32"=>"text32", "value33"=>"text33"],

    ],
    ["value11", "value22", "value33"]
);
$form->add_tag("fieldset");
$form->add_tag("label");
$form->add_tag_attributes([
    'for'=>"radio1",
    'text'=>"radio1"
]);
$form->add_tag("input");
$form->add_tag_attributes([
    'type'=>"radio",
    'id'=>"radio1",
    'name'=>"radio",
    'value'=>"radio1"
]);
$form->add_tag("label");
$form->add_tag_attributes([
    'for'=>"radio2",
    'text'=>"radio2"
]);
$form->add_tag("input");
$form->add_tag_attributes([
    'type'=>"radio",
    'id'=>"radio2",
    'name'=>"radio",
    'value'=>"radio2"
]);
$form->add_tag("label");
$form->add_tag_attributes([
    'for'=>"radio3",
    'text'=>"radio3"
]);
$form->add_tag("input");
$form->add_tag_attributes([
    'type'=>"radio",
    'id'=>"radio3",
    'name'=>"radio",
    'value'=>"radio3"
]);
$form->add_tag("fieldset");
$form->add_tag("input");
$form->add_tag_attributes([
    'type'=>"submit",
    'value'=>"enviar",
    'name'=>"enviar"
]);
echo $form->render(); // or $html = $form->render();
 *
 * -- Important --
 *
 * This code is written for PHP 7.0.33 so visibility, return types and some
 * other improvements are not available to me. However I tried to accomplish
 * PSR 12 as much as a I could.
 *
 * You will notice there are no escaping functions in use. This is intended
 * for a developer so any kind of validation is left for it.
 *
 * Thorough tests still need to be run but so long this class is working.
 *
 * -- Notes about my coding style --
 *
 * As much as I try to accomplish standards there are certain things I don't
 * like such as camel case for method names (and will ignore anything related
 * to this), using sprintf() for replacements or swith to `[]` instead of
 * `array()` (I use `array()` to declare multidimensional arrays and will
 * continue this way), just to name some of them.
 * They are not much and IMO they can be allowed.
 *
 * -- About this class --
 *
 * This class aims to render a valid code for HTML forms.
 * When this class is instantiated constructor takes an associative array
 * to set  attributes. Like this:
 *
 *     $form = new Form([
 *         "name" => "my_form",
 *         "action" => "destination.php",
 *         "method" => "post"
 *     ]);
 *
 * Those three attributes are mandatory. The others are optional.
 *
 * In general:
 *
 * parameters, when its type is array, are associative arrays, and
 *
 * mandatory attributes must be present and have a valid value, and
 *
 * if any optional attribute is set it must contain a non empty
 * string excepting boolean attributes such as `required`, `autofocus`,
 * `multiple` and `checked` (which can hold anything you want) or when the
 * attribute is `value`. When dealing with labels, textareas, fieldsets and
 * legends attribute `value` is named `text`, and
 *
 * custom or non standard attributes are not allowed and will be dismissed
 * with a user level warning (not implemented yet)
 *
 * To add a tag:
 *
 * $form->add_tag("tag_name");
 * $form->add_tag_attributes(["attr1"=>"value1", ...]);
 *
 * If intended tag is a select, options must be set:
 *
 * $form->add_tag_options(["opt1"=>"value1", ...]);
 *
 * If select tag has any pre-selected value:
 *
 * $form->add_tag_options(["opt1"=>"value1", ...], ["selected1", "selected2", ...]);
 *
 * This documentation is still in progress.
 */
class Form
{
    /** in here each valid tag is stored.
     * it is looped in render()
     * @var array
     */
    private $output = array();

    /**
     * used to hold tag name while building HTML
     * to reduce the number of times tag name should be passed as parameter
     * @var string
     */
    private $tag = "";

    /** used to build tag
     * @var string
     */
    private $tag_build = "";

    /**
     * flag for when a  is intended
     * @var boolean
     */
    private $select_options_required = false;

    /**
     * flag to check if 's are set
     * @var boolean
     */
    private $select_options_set = false;

    // as I'm using PHP 7.0.33 visibility is not allowed for constants
    // all this constants should be private

    const FORM_TAGS = ['form', 'input', 'label', 'select', 'textarea', 'button', 'fieldset', 'legend',
        'datalist', 'output', 'option', 'optgroup'];
    const FORM_TAGS_TYPES = array(
        'input' => ['button', 'checkbox', 'color', 'date', 'datetime-local', 'email', 'file', 'hidden', 'image', 'month',
            'number', 'password', 'radio', 'range', 'reset', 'search', 'submit', 'tel', 'text', 'time', 'url', 'week'],
        'button' => ['submit', 'reset', 'button'],
    );
    const FORM_TAGS_COMMON_ATTR = ['id', 'class', 'accesskey', 'style', 'tabindex'];
    const FORM_TAGS_REQUIRED_ATTR = array(
        'form' => ['name', 'action', 'method'],
        'select' => ['name'],
        'textarea' => ['name', 'cols', 'rows'],
        'button' => ['name', 'value'],
        'option' => ['value', 'text'],
        'input' => ['type', 'name', 'value'],
        'optgroup' => ['label'],
    );
    const FORM_TAGS_SPECIFIC_ATTR = array(
        'form' => ['target', 'enctype', 'autocomplete', 'rel', 'novalidate'],
        'label' => ['for'],
        'select' => ['autocomplete', 'autofocus', 'disabled', 'form', 'multiple', 'required', 'size'],
        'textarea' => ['autocomplete', 'autofocus', 'disabled', 'form', 'maxlength', 'minlength', 'placeholder', 'readonly', 'required'],
        'button' => ['autofocus', 'disabled', 'form', 'formaction', 'formenctype', 'formmethod', 'formtarget', 'formnovalidate'],
        'fieldset' => ['disabled', 'form'],
        'legend' => [],
        'datalist' => [],
        'output' => ['for', 'form'],
        'option' => ['disabled', 'label', 'selected'],
        'optgroup' => ['disabled'],
        'input' => ['disabled', 'required'],
        'checkbox' => ['checked'],
        'color' => [],
        'date' => ['max', 'min', 'step'],
        'datetime-local' => ['max', 'min', 'step'],
        'email' => ['list', 'maxlength', 'minlength', 'multiple', 'pattern', 'placeholder', 'readonly', 'size'],
        'file' => ['accept', 'capture', 'multiple'],
        'hidden' => [],
        'image' => ['alt', 'formaction', 'formenctype', 'formmethod', 'formnovalidate', 'formtarget', 'height', 'src', 'width'],
        'month' => ['list', 'max', 'min', 'readonly', 'step'],
        'number' => ['list', 'max', 'min', 'placeholder', 'readonly', 'step'],
        'password' => ['maxlength', 'minlength', 'pattern', 'placeholder', 'readonly', 'size'],
        'radio' => ['checked', 'required'],
        'range' => ['list', 'max', 'min', 'step'],
        'reset' => [],
        'search' => ['list', 'maxlength', 'minlength', 'pattern', 'placeholder', 'readonly', 'size', 'spellcheck'],
        'submit' => ['formaction', 'formenctype', 'formmethod', 'formnovalidate', 'formtarget'],
        'tel' => ['list', 'maxlength', 'minlength', 'pattern', 'placeholder', 'readonly', 'size'],
        'text' => ['autofocus', 'list', 'maxlength', 'minlength', 'pattern', 'placeholder', 'readonly', 'size', 'spellcheck'],
        'time' => ['list', 'max', 'min', 'readonly', 'step'],
        'url' => ['list', 'maxlength', 'minlength', 'pattern', 'placeholder', 'readonly', 'size', 'spellcheck'],
        'week' => ['max', 'min', 'readonly', 'step']
    );
    const FORM_TAGS_FORBIDDEN_ATTR = []; // to be implemented in the future
    // fieldsets closing is treated in render() method
    const FORM_TAGS_CLOSING = ['label', 'select', 'textarea', 'legend', 'option', 'optgroup'];
    const FORM_BOOLEAN_ATTR = ['required', 'autofocus', 'multiple', 'checked'];

    /**
     *
     * @param array $form Asociative array where the keys are the form tag
     * attributes and the values the attribute's value
     * @throws Exception
     *
     * Attributes name, action and method are mandatory and cannot be empty.
     * The rest of the attributes, if set must have a valid value.
     *
     * Usage:
     *
     *  $form = new Form(['name'=>'myform', 'action'=>'action.php', 'method'=>'post']);
     *
     * Result:
     *
     *  
     *
     */
    public function __construct(array $form_attributes)
    {
        $output = "";

        $this->check_attributes("form", $form_attributes);

        $output = ' $value) {
            $output .= " $attribute";
            if (!in_array($attribute, self::FORM_BOOLEAN_ATTR)) {
                $output .= "=\"$value\"";
            }
        }

        $output .= '>';

        $this->output[] = $output;
    }

    public function add_tag(string $element): bool
    {
        $this->check_unset_select();

        // as fieldsets are treated differently and when they have no
        // attributes set needs the trailing '>'
        if (
            ($this->tag == "fieldset") and
            (strpos($this->tag_build, -1) != ">")
        ) {
            $this->output[] = $this->tag_build . ">";
        }

        $this->tag = "";
        $this->tag_build = "";
        $this->select_options_required = false;
        $this->select_options_set = false;

        $this->check_element($element);

        $this->tag = $element;

        $this->tag_build = "<" . $element;

        if ($element == "select") {
            $this->select_options_required = true;
        }
        return true;
    }

    public function add_tag_attributes(array $attributes): bool
    {
        $this->check_attributes($this->tag, $attributes);

        if (array_key_exists($this->tag, self::FORM_TAGS_TYPES)) {
            $this->check_element_type($attributes["type"]);
        }

        foreach ($attributes as $attribute => $value) {
            if ($attribute != "text") {
                $this->tag_build .= " $attribute";

                if (!in_array($attribute, self::FORM_BOOLEAN_ATTR)) {
                    $this->tag_build .= "=\"$value\"";
                }
            }
        }

        $this->tag_build .= ">";

        if (in_array($this->tag, ["label", "textarea", "legend"])) {
            if (array_key_exists("text", $attributes)) {
                $this->tag_build .= $attributes["text"] ?? "";
            }
            $this->tag_build .= "tag . ">";
        }

        if ($this->tag != "select") {
            $this->output[] = $this->tag_build;
        }

        return true;
    }

    public function add_tag_options(array $options, array $selected = []): bool
    {
        $optgroup = false;

        if ($this->tag != "select") {
            throw new Exception("Options are only valid for select tag");
        }

        if (count($options) < 1) {
            throw new Exception("select tag must have at least one option");
        }

        // check if optgroup
        if (count($options) != count($options, COUNT_RECURSIVE)) {
            $optgroup = true;
        }

        foreach ($options as $value => $text) {
            if ($optgroup) {
                $this->tag_build .= "\n";
                foreach ($text as $opt_value => $opt_text) {
                    $this->tag_build .= "\ntag_build .= " selected";
                    }

                    $this->tag_build .= ">$opt_text";
                }
                $this->tag_build .= "\n"; // this is not mandatory
            } else {
                $this->tag_build .= "\ntag_build .= " selected";
                }

                $this->tag_build .= ">$text";
            }
        }

        $this->tag_build .= "\n";

        $this->select_options_set = true;
        $this->output[] = $this->tag_build;
        unset($this->tag_build);
        return true;
    }

    public function render(): string
    {
        $open_fieldsets = 0;
        $output = "";

        $this->check_unset_select();

        foreach ($this->output as $element) {
            if (strpos($element, "fieldset") !== false) {
                if ($open_fieldsets > 0) {
                    $output .= "\n";
                    $open_fieldsets--;
                }
                $open_fieldsets++;
            }
            $output .= "\n$element";
        }

        while ($open_fieldsets != 0) {
            $output .= "\n";
            $open_fieldsets--;
        }

        $output .= "\n";
        unset($this->output); // prevents double rendering
        return $output;
    }

    /**
     * Checks if an HTML form tag is valid
     * @param string $declared_element
     * @throws Exception
     * @return boolean
     *
     * As said, PHP 7.0.33 does not allow void return type
     *
     */
    private function check_element (string $declared_element): bool
    {
        if (strlen($declared_element) < 1) {
            throw new Exception("Form tag/element must be a non empty string");
        }

        if (in_array($declared_element, self::FORM_TAGS)) {
            return true;
        }

        throw new Exception("'$declared_element': illegal form (or type of) element");
    }

    /**
     * Checks if input or button has an allowed type
     * @param string $type
     * @throws Exception
     * @return bool
     *
     * As said, PHP 7.0.33 does not allow void return type
     */
    private function check_element_type(string $type): bool
    {
        if (strlen($type) == 0) {
            throw new Exception($this->tag . " type cannot be an empty string");
        }

        foreach (self::FORM_TAGS_TYPES as $element => $types) {
            if (
                ($this->tag == $element) and
                in_array($type, $types)
            ) {
                return true;
            }
        }

        throw new Exception("'$type' type for '" . $this->tag . "' is not valid");
    }

    /**
     * Checks attributes
     *
     * @param string $element
     * @param array $attributes
     * @throws Exception
     * @return bool
     *
     * This method checks for mandatory attributes to be present. Also checks
     * for non mandatory and common attributes and ignores the rest.
     *
     * As said, PHP 7.0.33 does not allow void return type
     */
    private function check_attributes (string $element, array $attributes): bool
    {

        foreach (self::FORM_TAGS_REQUIRED_ATTR[$element] ?? [] as $attribute) {

            if (!array_key_exists($attribute, $attributes)) {
                throw new Exception("Attribute '$attribute' for tag '$element' is mandatory");
            }

            // if attribute is not value or text, and it's not a boolean
            // attribute and its length is 0... not valid
            if (
                ($attribute != "value") and
                ($attribute != "text") and
                (!in_array($attribute, self::FORM_BOOLEAN_ATTR)) and
                (strlen($attributes[$attribute]) == 0)
            ) {
                throw new Exception("Attribute '$attribute' value cannot be an empty string");
            }
        }

        return true;
    }

    /**
     * Check's if select tag options where set
     * @throws Exception
     * @return bool
     *
     * This function is used in add_tag() and render() methods.
     *
     * As said, PHP 7.0.33 does not allow void return type
     */
    private function check_unset_select(): bool
    {
        // check if previous tag was "select" and if options were set
        if (
            ($this->tag == "select") and
            !$this->select_options_set
        ) {
            throw new Exception("Options for select are mandatory");
        }

        return true;
    }

}
EN

回答 1

Code Review用户

回答已采纳

发布于 2023-05-03 19:03:00

评论

关于代码本身,关于数组参数、效率和易用性的方法,有什么建议吗?

自从第一个版本发布以来,代码已经走了很长的一段路。

乍一看,这似乎并没有太低效率。对check*方法的调用通常出现在主处理之前的方法中(例如,遍历属性)。

如果有优化速度的目标,那么可以考虑使用简单的for循环而不是foreach循环。我并不期望速度会有很大的提高,但是当创建一个库时,需要考虑一些问题。

建议

考虑支持

版本

我使用的是PHP 7.0.33

如果您控制了PHP版本,那么更新它将是明智的,如果没有,那么请求更新它将是明智的。在编写对8.1和8.2版有积极的支持,只有8.0的安全性修复7.2及更高版本的终止生命支持时。因此,像7.0.33这样的版本可能会暴露出未修补的安全漏洞。此外,还有PHP8提高了性能和许多其他特性。

使用不同的字符串描述器

Form构造函数中,对属性有一个循环。在循环中是这个条件块:

if (!in_array($attribute,self::FORM_BOOLEAN_ATTR)) { $output .=“=\”$value\“;}

HTML规范允许使用指定属性值的多种方法,包括单引号属性值语法双引号属性值语法,因此可以使用单引号,这不需要转义字符:

代码语言:javascript
复制
if (!in_array($attribute, self::FORM_BOOLEAN_ATTR)) {
    $output .= "='$value'";
}

在可能的情况下使用严格的类型比较(

)

只要有可能就会有使用严格的相等运算符是一个好习惯。。它可能会更快,因为不需要类型铸造。已经有一些严格的类型比较-例如,在render方法中:

($this->输出为$element) { if (strpos($element,"fieldset") !== false) {

add_tag()方法中,有几个松散的等式检查:

if ($this->tag == "fieldset")和(strpos( $this->tag_build,-1) != ">") { $this->output[] =$this->tag_build。“>”

最后:

if ($element == "select") {

属性$this->tag是一个字符串,参数$element有一个声明为:string的类型,因此可以在两处使用严格的相等。

-重新考虑给定优先

的运算符

在前面代码的米克马库萨评论中,提到了以下内容:

  1. 我个人从不在PHP代码中使用andor。这有助于避免优先级问题,并确保代码的一致性。

PHP有逻辑的两个变体AND,即and&& (以及逻辑ORor||)。请注意,and优先级比许多其他运算符低。包括赋值- =。上面的代码仍然使用带括号的条件的and。在括号可能不存在的情况下,使用&&将是一个好习惯。正如这个StackOverflow的答案所说明的,它可能在某些场景中导致意外的后果。

票数 2
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/284793

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档