TypeRocket / typerocket

TypeRocket is a highly integrated MVC WordPress framework with beautiful UI components for the modern developer.
https://typerocket.com
454 stars 62 forks source link

[feature request] Support HTTP Fields in form #237

Closed jdoubleu closed 3 years ago

jdoubleu commented 3 years ago

It would be nice, if the Form also supports HTTP Fields, in the same sense it supports a data collection.

Right now, if you pass a Fields object to the form, it gets wrapped by a DataCollection, basically making it useless. No fields will be extracted from the actual Fields object, because the DataCollection is treated as the primary model.

There are two options how to implement this feature, I could think of right now:

  1. Implement the Formable interface in the Fields class. Formable::getFieldValue can be wrapped around Fields::get.
  2. Extend the "auto detection" mechanism of the form and add an additional if for Fields classes, right before the is_object branch.

I think this should be part of the core, even though option 1 works for everyone implementing it themselves.

kevindees commented 3 years ago

Hey @jdoubleu

Does this work for you?

<?php
namespace TypeRocket\Http;

use TypeRocket\Interfaces\Formable;
use TypeRocket\Models\Traits\FieldValue;
use TypeRocket\Utility\Data;
use TypeRocket\Utility\Validator;

class Fields extends \ArrayObject implements Formable
{
    use FieldValue;

    protected $fillable = [];
    protected $rules = [];
    protected $messages = [];
    protected $messagesRegex = true;
    protected $run;
    /** @var Validator|null */
    protected $validator;
    /** @var string Model calss for validator */
    protected $modelClass;
    /** @var string Requests fields group using dot notation to pass data to model */
    protected $modelFieldsGroup;

    /**
     * Load commands
     *
     * @param array|null $fields
     *
     * @throws \TypeRocket\Exceptions\RedirectError
     * @throws \Exception
     */
    public function __construct( $fields = null ) {
        parent::__construct();

        if( empty($fields) ) {
            $fields = (new Request)->getFields();
        }

        $this->exchangeArray( $fields ?? [] );
        $this->fillable = array_merge($this->fillable, $this->fillable());
        $this->rules = array_merge($this->rules, $this->rules());
        $this->messages = array_merge($this->messages, $this->messages());

        if($this->run) {
            $this->runAndRespond();
        }
    }

    /**
     * Run Fields Validation
     *
     * @param null|string $type
     */
    public function runAndRespond($type = null)
    {
        $this->run = $type ?? $this->run;

        if(!$this->validator) {
            $this->validate();
        }

        if($this->run !== 'response') {
            $this->validator->redirectWithErrorsIfFailed([$this, 'afterRespond']);
        }

        $this->validator->respondWithErrors([$this, 'afterRespond']);
    }

    /**
     * @param Response|Redirect $object
     *
     * @throws \Exception
     */
    public function afterRespond($object)
    {
        if($object instanceof Redirect) {
            $this->redirect($object);
        }

        if($object instanceof Response) {
            $this->response($object);
        }
    }

    /**
     * Get Field
     *
     * @param array|string|null $key dot notation key.next.final
     * @param mixed $default
     *
     * @return array|mixed|object|null
     */
    public function get($key = null, $default = null)
    {
        $data = $this->getArrayCopy();
        $value = is_null($key) ? $data : Data::walk($key, $data);

        return $value ?? $default;
    }

    /**
     * @return array
     */
    public function getFormFields()
    {
        $data = $this->get();
        $result = [];

        foreach ($data as $i => $item) {
            if($item instanceof Formable) {
                $result[$i] = $item->getFormFields();
            } else {
                $result[$i] = $item;
            }
        }

        return $result;
    }

    /**
     * Get Fields For Model
     *
     * @return array|mixed|object|null
     */
    public function getModelFields()
    {
        return $this->get($this->modelFieldsGroup);
    }

    /**
     * @return array
     */
    protected function fillable() {
        return [];
    }

    /**
     * @return array
     */
    protected function rules() {
        return [];
    }

    /**
     * @return array
     */
    protected function messages() {
        return [];
    }

    /**
     * @param Redirect $redirect
     *
     * @return Redirect
     */
    public function redirect(Redirect $redirect)
    {
        return $redirect;
    }

    /**
     * @param Response $response
     *
     * @return Response
     */
    public function response(Response $response)
    {
        return $response;
    }

    /**
     * @return array
     */
    public function getRules()
    {
        return $this->rules;
    }

    /**
     * @param array $rules
     *
     * @return $this
     */
    public function setRules(array $rules)
    {
        $this->rules = $rules;

        return $this;
    }

    /**
     * @return array
     */
    public function getMessages()
    {
        return $this->messages;
    }

    /**
     * @param array $messages
     *
     * @return $this
     */
    public function setMessages(array $messages)
    {
        $this->messages = $messages;

        return $this;
    }

    /**
     * Get fillable
     *
     * @return array
     */
    public function getFillable()
    {
        return $this->fillable;
    }

    /**
     * Set fillable
     *
     * @param array $fillable
     *
     * @return $this
     */
    public function setFillable(array $fillable)
    {
        $this->fillable = $fillable;

        return $this;
    }

    /**
     * Validate fields
     *
     * @param array|null $rules
     * @param string|null $modelClass
     *
     * @return Validator
     * @throws \Exception
     */
    public function validate($rules = null, $modelClass = null)
    {
        if( ! $rules ) {
            $rules = $this->rules;
        }

        if( empty($rules) ) {
            throw new \Exception('No options for validator set.');
        }

        if( $this->validator ) {
            throw new \Exception('Validation already run.');
        }

        $this->validator = new Validator($rules, $this->getArrayCopy(), $modelClass ?? $this->modelClass, false);

        if(!empty($this->messages)) {
            $this->validator->setErrorMessages($this->messages, $this->messagesRegex);
        }

        $this->beforeValidate($this->validator);
        $this->validator->validate(true);
        $this->afterValidate($this->validator);

        return $this->validator;
    }

    /**
     * @param Validator $validator
     */
    public function beforeValidate($validator)
    {
    }

    /**
     * @param Validator $validator
     */
    public function afterValidate($validator)
    {
    }

    /**
     * @return mixed|Validator
     */
    public function getValidator()
    {
        return $this->validator;
    }

    /**
     * @param mixed ...$args
     *
     * @return static
     */
    public static function new(...$args)
    {
        return new static(...$args);
    }
}
jdoubleu commented 3 years ago

Yes it does. Great, thanks!

Although I'm not sure, whether

public function getFormFields()
    {
        $data = $this->get();
        $result = [];

        foreach ($data as $i => $item) {
            if($item instanceof Formable) {
                $result[$i] = $item->getFormFields();
            } else {
                $result[$i] = $item;
            }
        }

        return $result;
    }

is really necessary, here. It just needs to call Fields::get(), right? In the same sense, FieldValue::getFieldValue() could easily be replaced by:

public function getFieldValue($field)
{
    return $this->get($field->getDots());
}

I'm not sure if someone would put a Formable into their custom Fields class.

On the other hand, I see the point in using that unified FieldValue trait.

kevindees commented 3 years ago

Hey @jdoubleu

I agree on the get(). However, I can see someone maybe casting a field name to an object and I'd like to be safe just in case of that situation.

Thanks, Kevin