2
\$\begingroup\$

My goal is to make a form builder class, that generates bootstrap 4 form inside a modal. My doubt is whether I should leave it as is or if I should crate a interface and then implement that interface for all types.
Also is there a better way to write html inside php?

Class to generate the form

class BS4Form
{
    protected $action = "";
    protected $id = "";
    protected $title = "";
    protected $fields = array();

    public function __construct($action, $id, $title)
    {
        $this->action = $action;
        $this->id = $id;
        $this->title = $title;
        //$this->fields = $fields;
    }

    /**
     * @param mixed $fields
     */
    public function setFields($field): void
    {
        array_push($this->fields, $field);
    }

    public function renderForm() : void
    {
        ?>
        <div class="modal fade" id="<?= $this->id ?>" tabindex="-1" role="dialog" aria-labelledby="<?= $this->id ?>Title" aria-hidden="true">
            <div class="modal-dialog modal-dialog-centered" role="document">
                <div class="modal-content">
                    <div class="modal-header">
                        <h5 class="modal-title" id="<?= $this->id ?>Title"><?= $this->title ?></h5>
                        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                            <span aria-hidden="true">&times;</span>
                        </button>
                    </div>
                    <div class="modal-body">
                        <form action="<?= $this->action ?>.php" method="POST" id="form_<?= $this->id ?>">
                            <?php foreach ($this->fields as $field ): ?>
                                <?php $field->renderField(); ?>
                            <?php endforeach; ?>
                        </form>
                    </div>
                    <div class="modal-footer">
                        <button type="button" class="btn btn-danger" data-dismiss="modal">Cancelar</button>
                        <button type="submit" class="btn btn-success" form="form_<?= $this->id ?>">Guardar</button>
                    </div>
                </div>
            </div>
        </div>
        <?php
    }

}

Sub-class to generate the fields

class BS4Field
{
    protected $title = "";
    protected $type = "";
    protected $id = "";
    protected $name = "";
    protected $value = "";

    public function __construct($title, $type, $id, $name, $value)
    {
        $this->title = $title;
        $this->type = $type;
        $this->id = $id;
        $this->name = $name;
        $this->value = $value;
    }

    protected function renderFormGroup() : void
    {
        ?>
        <div class="form-group">
            <label for="<?= $this->id ?>"><?= $this->title ?></label>
            <input class="form-control" type="<?= $this->type ?>" id="<?= $this->id ?>" name="<?= $this->name ?>" placeholder="<?= $this->value ?>">
        </div>
        <?php
    }

    protected function renderFormCheck() : void
    {
        ?>
        <div class="form-check">
            <input class="form-check-input" type="<?= $this->type ?>" value="<?= $this->value ?>" name="<?= $this->name ?>" id="<?= $this->id ?>">
            <label class="form-check-label" for="<?= $this->id ?>"><?= $this->title ?></label>
        </div>
        <?php
    }

    public function renderField() : void
    {
        ($this->type === "checkbox") ? $this->renderFormCheck() : $this->renderFormGroup();
    }
}

Example of usage

$form = new BS4Form("", "modalEditar", "Editar Utilizador");

$form->setFields(new BS4Field("Ativo", "number", "ativo", "ativo", "ex: 12"));
$form->setFields(new BS4Field("Email", "email", "emailUser", "emailUser", "example@example.com"));
$form->setFields(new BS4Field("Token", "text", "authToken", "authToken", "Token"));
$form->setFields(new BS4Field("Renovado", "number", "renovado", "renovado", "0"));
$form->setFields(new BS4Field("Admin", "checkbox", "isAdmin", "isAdmin", "0"));

$form->renderForm();
\$\endgroup\$
5
  • \$\begingroup\$ BS4Field is not a subclass. A subclass inherits from a parent class. Should BS4Field implement an interface? Yes, I think it should. This is because the interface is a contract that guarantees that BS4Field will implement the required renderField() method that BS4Form relies on. I think you can see the direction that BS4Field is going to go, when you look at what you hardwired for renderField() for checkbox. This tells me that you should have an abstract base class for BS4Field and individual subclasses for each different form element you want to support \$\endgroup\$ – gview May 8 '20 at 4:17
  • \$\begingroup\$ You can use the BS4Field interface to typehint the parameter you are passing. This will allow you to write form element specific code as you like without having to change existing working form element classes, or having to change the form class. One last thing that I would change public function setFields($field): void. This name does not match the actual behavior. I would call it addField(), as it adds a single field to the array of form fields at a time. \$\endgroup\$ – gview May 8 '20 at 4:19
  • \$\begingroup\$ What version of PHP are we talking about here? \$\endgroup\$ – slepic May 8 '20 at 6:19
  • \$\begingroup\$ @slepic it's php 7 \$\endgroup\$ – David Machado May 8 '20 at 8:09
  • \$\begingroup\$ The current question title of your question is too generic to be helpful. Please edit to the site standard, which is for the title to simply state the task accomplished by the code. Please see How do I ask a good question?. \$\endgroup\$ – BCdotWEB Oct 7 '20 at 14:16
1
\$\begingroup\$

PHP 7 features

I don't know exactly your PHP version. 7 is unfortunately not enough specific. A lot of useful thing were added in minor releases. But anyway you should use all those typehints and other stuff where it makes sense. In my code snippets I'll just write using 7.4.

Rendering inline HTML

If it is at least PHP 7.3, you can use indented heredoc for the inline HTML.

  public function render(): string
  {
    return <<<HTML
    <div>the indent before this div is not in the output since PHP 7.3</div>
      <div>but only as long as the terminator has the same indent level</div>
      <div>these two divs will be indented on output</div>
    <div>You can also even show some $var or {$this->var} like in double quoted string literal.</div>
    <div>but remember you still have to escape it</div>
    <div>The HTML delimiter can be anything, but it must be same on start and end<div>
    <div>so after all a templating system might be better</div>
    <<<HTML
  }

Feel free to abstract yourself from this problem for now with this php package I wrote for that exact reason. https://packagist.org/packages/slepic/php-template

(sry for the promo :))

Polymorphism

The input class has a different behaviour if it is a checkbox, and different for all other cases (It probably does not fit select input and some others too). You should model this as polymorphism, in other words as separate classes with the same interface (or base class if you wish).

You may even want a separate class for modal box, and a form.

Let me show you some structure. I will assume we have imported the php-template library and use its TemplateInterface which looks like this:

interface TemplateInterface
{
    public function render(array $data): string;
}

We use it to separate the HTML rendering away from those classes. Making it generic, unaware of BS4 backing it up, and agnostic of any template engine used (the package includes a pure php "no template engine" template interface implementation called OutputBufferTemplate)

Lets define common interface for things that can be rendered.

interface Component
{
  public function render(): string;
}

Lets define modal box component which should show another component in a modal box.

class ModalBoxComponent implements Component
{
  private TemplateInterface $template;
  private IComponent $content;
  private int $width;
  private int $height;

  public function __construct(TemplateInterface $template, Component $content, int $width, int $height)
  {
    $this->template = $template;
    $this->content = $content;
    $this->width = $width;
    $this->height = $height;
  }

  public function render(): string
  {
    return $this->template->render([
      'content' => $this->content->render(),
      'width' => $this->width,
      'height' => $this->height,
    ]);
  }
}

Lets define form component that holds mutiple input components nad has action, method, etc...

class FormComponent implements Component
{
  private TemplateInterface $template;
  private string $action;
  private array $inputs;
  // ...

  /**
   * @param Component[] $inputs
   */
  public function __construct(TemplateInterface $template, string $action, array $inputs)
  {
    $this->template = $template;
    $this->action = $action;
    $this->inputs = $inputs;
  }

  public function render(): string
  {
    return $this->template->render([
      'inputs' => \array_map(fn($input) => $input->render(), $this->inputs),
      'action' => $this->action,
      ... 
    ]);
  }
}

Some component for common inputs

class InputComponent implements Component
{
  private string $title;
  private string $type;
  private string $id;

  // constructor, template, other stf.., etc.
}

checkbox maybe extra, or maybe it just needs different template?

class CheckboxComponent implements Component
{
  // ... and so on
}

Something to create templates for the form and inputs.

interface FormTemplateAbstractFactory
{
  public function createNumberTemplate(): TemplateInterface;
  public function createCheckboxTemplate(): TemplateInterface;
  public function createFormTemplate(): TemplateInterface;
}

class BS4FormTemplates implements FormTemplateAbstractFactory
{
  public function createNumberTemplate(): TemplateInterface
  {
    return new OutputBufferTemplate($this->templatesDir . '/bs4input.php');
  }

  public function createCheckboxTemplate(): TemplateInterface
  {
    return new OutputBufferTemplate($this->templatesDir . '/bs4checkbox.php');
  }

  public function createFormTemplate(): TemplateInterface
  {
    return new OutputBufferTemplate($this->templatesDir . '/bs4form.php');
  }
}

now something to simplify putting those things together

class FormBuilder
{
  private FormTemplateAbstractFactory $templates;

  public function __construct(FormTemplateAbstractFactory $templates)
  {
    $this->templates = $templates;
  }

  public function addNumber(string $name, ?string $label = null): void
  {
    $this->inputs[$name] = new InputComponent(
      $this->templates->createNumberTemaplte(),
      'number',
      $name,
      $label,
      // ...
    );
  }

  public function addCheckbox(string $name, ?string $label = null): void
  {
    // here Im showing the variant where checkbox is just input with different template, it is not a separate class (depends what you want from it)
    $this->inputs[$name] = new InputComponent(
      $this->templates->createCheckboTemaplte(),
      'checkbox',
      $name,
      $label,
      // ...
    );     
  }

  // passwords, texts, textareas, selectboxes, and more...

  public function buildForm(): FormComponent
  {
    return new FormCompoennt(
      $this->templates->createFormTemaplte(),
      $this->inputs,
    );
  }
}

And we are ready to create a form

$builder = new FormBuilder(new BS4FormTemplates());
$builder->addText('name', "Your name");
$builder->addNumber('age', "Your age");
$builder->addPassword('password', "Your secret");
$form = $builder->buildForm();

Put it in a modal box.

$modal = new ModalBoxComponent(
  new OutputBufferTemplate($myModalBoxTemplateFilePath),
  $form
);

And show it

echo $modal->render();

Now OutputBufferTemplate will execute an external php file and gather its output and so it can be good old plain html with inserted php pieces. some_template.php:

<div>
  <div class="title"><?= htmlspecialchars($title) ?></div>
  <div><?= $content ?></div>
</div>

and render it with

$temaplte = new OutputBufferTemplate('some_template.php');
echo $template->render(['content' => '<div>abc</div>', 'title' => 'a<b']);

I'm sorry my will to write things properly gradully decreased as I wrote this, because it's already after midnight here :D So I went from writing complete code to only showing the basic structure, I hope it helps nevertheless :) I'll try to stop by to improve it..

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.