DEV Community

Emanuel Vintila
Emanuel Vintila

Posted on

Creating a HTML form for a class using PHP and Reflection

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

Now, we only need to write a single line in our index.php

echo FormHelper::MakeForm(Article::class);
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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.

Latest comments (1)

Collapse
 
willschubert1977 profile image
Will Schubert

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