在我之前的问题之后 I将代码修改为下面的代码。我正在使用PHP 7.0.33,并试图尽可能多地完成PSR-12。在docblock中的“浓缩用法”中,您可以找到一个完整的复制粘贴示例。
以下是代码:
/**
* 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;
}
}发布于 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规范允许使用指定属性值的多种方法,包括单引号属性值语法或双引号属性值语法,因此可以使用单引号,这不需要转义字符:
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的类型,因此可以在两处使用严格的相等。
的运算符
在前面代码的米克马库萨评论中,提到了以下内容:
and或or。这有助于避免优先级问题,并确保代码的一致性。PHP有逻辑的两个变体AND,即and和&& (以及逻辑OR:or和||)。请注意,and优先级比许多其他运算符低。包括赋值- =。上面的代码仍然使用带括号的条件的and。在括号可能不存在的情况下,使用&&将是一个好习惯。正如这个StackOverflow的答案所说明的,它可能在某些场景中导致意外的后果。
https://codereview.stackexchange.com/questions/284793
复制相似问题