This is a cross post from my own blog: Creating a HTML form for a class
Have you ever thought Could there possibly be an easier way to create a form rather than plainly writing out the markup?. Assuming your form represents an object that can be represented as a class, there most certainly is; and if not, then it most definitely should. Let us create an overly-simplified article on a blog.
class Article {
/** @var string */
public $title = '';
/** @var string */
public $body = '';
/** @var DateTime */
public $date;
public function __construct() {
// this will be explained when we get to re-creating the object
// after a POST request
$this->date = $this->date ?
new DateTime($this->date) :
new DateTime();
}
}
We want the title and body to be plain text inputs, and the date to be a date input. We are going to handle both the displaying of the form through a GET request, and the submission of the form through a POST request. Let us begin by enumerating our class's properties, creating an input for each one, and also wrapping them in a form tag with a submit input. Let us get straight to reflection. I have only annotated the MakeInput
function as the other two should be self-explanatory.
/**
* This function creates a valid HTML input with an associated label.
* @param string $name The input's name and the label's text.
* @param mixed $value The input's initial value. This parameter also determines the input's type.
* @return string
*/
public function MakeInput(string $name, $value): string
{
$type = gettype($value);
$label = sprintf('<label for="%1$s">%1$s</label>', $name);
switch ($type) {
case 'boolean':
$input_type = 'checkbox';
break;
case 'integer':
case 'double':
$input_type = 'number';
break;
case 'string':
$input_type = 'text';
break;
case 'object':
// special handling for object types
$class = new ReflectionClass($value);
if ($class->implementsInterface(DateTimeInterface::class)) {
$input_type = 'date';
$value = $class
->getMethod('format')
->invoke($value, 'Y-m-d');
break;
}
// if we do not know how to handle the object, fall-through and throw
default:
throw new InvalidArgumentException($value);
}
$input = sprintf('<input name="%1$s" id="%1$s" type="%2$s" value="%3$s" />',
$name, $input_type, $value);
return $label . $input;
}
public function MakeInputs(string $class_name): array
{
$inputs = [];
$instance = new $class_name();
$class = new ReflectionClass($instance);
$properties = $class->getProperties();
foreach ($properties as $property) {
// make it accessible so we can get its value
$property->setAccessible(true);
$name = $property->getName();
$value = $property->getValue($instance);
// this syntax means that after submission our $_POST superglobal
// will contain an array named $class_name which will represent
// our class
$inputs[] = self::MakeInput("{$class_name}[{$name}]", $value);
}
return $inputs;
}
public static function MakeForm(string $class_name): string
{
$html = '<form method="POST">';
foreach (self::MakeInputs($class_name) as $input)
$html .= $input;
$html .= '<input type="submit" />';
$html .= '</form>';
return $html;
}
Now, we only need to write a single line in our index.php
echo FormHelper::MakeForm(Article::class);
And there we have our form. It is not the most beautiful looking form, but the markup is valid and it works. We could add a callback function as a parameter that adds classes or wraps every input in a container, but that is beyond the scope of this post. Let us address the submission part of the form now, that is, the POST request and the re-creation of our Article
object.
public function MakeClassFromArray(string $class_name, array $values)
{
$class = new ReflectionClass($class_name);
// we do not call the constructor yet
$instance = $class->newInstanceWithoutConstructor();
// first we set each property to their respective value
foreach ($values as $name => $value) {
$property = $class->getProperty($name);
$property->setAccessible(true);
$property->setValue($instance, $value);
}
// note that we have set primitive values to our object properties
// we late-call the constructor, like PDO does when fetching objects
// and it re-creates the object instances from the primitive values
$class->getConstructor()->invoke($instance);
return $instance;
}
Our index.php
becomes
$request_method = strtoupper($_SERVER['REQUEST_METHOD']);
$class = Article::class;
if ($request_method === 'GET') {
echo MakeForm($class);
} elseif ($request_method === 'POST') {
$article = MakeClassFromArray($class, $_POST[$class]);
print_r($article);
}
And there you have it, after submitting the form, the $article
variable was re-created from the request and is ready to be validated and stored into the database.
In the next post I will present another way of creating the form, one which will solve the issues this one has, such as properties having to be initialized to a default value, and the fact that the body property is represented by a plain input field rather than a textarea. Do not worry, it also involves reflection.
Top comments (1)
Whis thy code doesn't work? All functions declarations give this error:
Parse error: syntax error, unexpected 'public' (T_PUBLIC), expecting end of file in...
(this first line)
<?php
public function MakeInput(string $name, $value): string