nickwest/EloquentForms

View on GitHub
src/FormTrait.php

Summary

Maintainability
C
7 hrs
Test Coverage
<?php

namespace Nickwest\EloquentForms;

use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Nickwest\EloquentForms\Exceptions\NotImplementedException;
use Nickwest\EloquentForms\Traits\DataFromMySQL;

trait FormTrait
{
    // Added functionality for pulling extra column data from MySQL
    use DataFromMySQL;

    /**
     * Form object see Nickwest\EloquentForms\Form.
     *
     * @var Form
     */
    protected $Form = null;

    /**
     * @var array
     */
    protected $valid_columns = [];

    /**
     * @var array
     */
    protected $columns = [];

    /**
     * @var string
     */
    protected $blank_select_text = '-- Select One --';

    /**
     * @var string
     */
    protected $multi_delimiter = '|';

    /**
     * @var bool
     */
    public $validate_on_save = true;

    /**
     * Boot the trait. Adds an observer class for form.
     *
     * @return void
     */
    public static function bootFormTrait()
    {
        static::observe(new FormObserver);
    }

    /**
     * Constructor.
     *
     * @return mixed
     */
    public function __construct(array $attributes = [], bool $validate_on_save = true)
    {
        $this->validate_on_save = $validate_on_save;

        if (method_exists(get_parent_class($this), '__construct')) {
            return parent::__construct($attributes);
        }
    }

    /**
     * Access for validate_on_save.
     *
     * @return bool
     */
    public function validateOnSave()
    {
        return $this->validate_on_save;
    }

    /**
     * Get the form object.
     *
     * @return Form
     */
    public function Form(): Form
    {
        if (! is_object($this->Form)) {
            $this->Form = new Form();
        }

        return $this->Form;
    }

    /**
     * return an array of columns.
     *
     * @return array
     */
    public function getColumnsArray(): array
    {
        return $this->getAllColumns();
    }

    /**
     * Make a View for the form and return the rendered output.
     *
     * @param  array  $blade_data
     * @param  string  $extends
     * @param  string  $section
     * @param  bool  $view_only
     * @return View
     */
    public function getFormView(array $blade_data, string $extends = '', string $section = '', bool $view_only = false): \Illuminate\View\View
    {
        return $this->Form()->makeView($blade_data, $extends, $section, $view_only);
    }

    /**
     * Make a View for the field and return the rendered output.
     *
     * @param  string  $field_name
     * @param  array  $options
     * @return View
     */
    public function getFieldView(string $field_name): \Illuminate\View\View
    {
        return $this->Form()->$field_name->makeView();
    }

    /**
     * Make a View for the field and return the rendered output.
     *
     * @param  string  $field_name
     * @param  array  $options
     * @return View
     */
    public function getFieldDisplayView(string $field_name, array $options = []): \Illuminate\View\View
    {
        return $this->Form()->$field_name->makeDisplayView();
    }

    /**
     * String to append to each label.
     *
     * @param  string  $suffix
     * @return void
     */
    public function setLabelSuffix(string $suffix)
    {
        foreach ($this->Form()->getFields() as $Field) {
            $Field->label_suffix = $suffix;
        }
    }

    /**
     * Set the values from a post data array to $this model,
     * returned bool indicates if anything changed.
     *
     * @param  array  $post_data
     * @return void
     */
    public function setPostValues(array $post_data): void
    {
        foreach ($post_data as $field_name => $value) {
            // We don't throw Exceptions here because post fields come from users and could be anything
            // We just silently ignore invalid post fields
            if ($this->isColumn($field_name) && $this->isFillable($field_name)) { //TODO: should we limit to only display fields here too?
                if (is_object($this->Form()->{$field_name}->CustomField)) {
                    try {
                        $value = $this->Form()->{$field_name}->CustomField->hook_setPostValues($value);
                    } catch (NotImplementedException $e) {
                    }
                }

                $this->Form()->{$field_name}->attributes->value = $value;
                $this->{$field_name} = (is_array($value) ? implode($this->multi_delimiter, $value) : $value);
            }
        }

        // Make sure no Form fields were omitted from the post array (checkboxes can be when none are set)
        foreach ($this->Form()->getDisplayFields() as $Field) {
            if (isset($post_data[$Field->attributes->name]) || ! $this->isFillable($Field->getOriginalName())) {
                continue;
            }

            // If this is a boolean checkbox, set it to false when it's missing.
            if ($this->Form()->{$Field->getOriginalName()}->attributes->type == 'checkbox' && count($this->Form()->{$Field->getOriginalName()}->options->getOptions()) == 1) {
                $this->Form()->{$Field->getOriginalName()}->attributes->value = false;
                $this->{$Field->getOriginalName()} = false;
            }
            // If they were omitted set it to null
            elseif ($this->Form()->{$Field->getOriginalName()}->attributes->value != '') {
                $this->Form()->{$Field->getOriginalName()}->attributes->value = null;
                $this->{$Field->getOriginalName()} = null;
            }
        }
    }

    /**
     * Set all of the form values to whatever the value on that attribute of the model is.
     *
     * @return void
     */
    public function setAllFormValues(): void
    {
        foreach ($this->Form()->getFields() as $Field) {
            // Use the Model Field's value if it has the field
            if (isset($this->{$Field->getOriginalName()})) {
                $value = $this->{$Field->getOriginalName()};
            }
            // Use the Field's Default value if it has one
            elseif ($this->Form()->{$Field->getOriginalName()}->default_value != null) {
                $value = $this->Form()->{$Field->getOriginalName()}->default_value;
            }
            // Just use null
            else {
                $value = null;
            }

            // If the field has a CustomField object, then try to use it's hook for setting them value.
            if (is_object($Field->CustomField)) {
                try {
                    $this->Form()->{$Field->getOriginalName()}->attributes->value = $Field->CustomField->hook_setAllFormValues($Field, $value);
                    continue;
                } catch (NotImplementedException $e) {
                }
            }

            // If it's a checkbox or otherwise has multi_key set, assume we have a divided string that needs to be made into an array.
            if ($value !== null && ! is_array($value) && ($Field->attributes->type == 'checkbox' || $Field->attributes->multi_key)) {
                $value = explode($this->multi_delimiter, $value);
            }

            $this->Form()->{$Field->getOriginalName()}->attributes->value = $value;
        }
    }

    /**
     * Create the Form object from Json.
     *
     * @param  string  $json
     * @return void
     */
    public function generateFormFromJson(string $json): void
    {
        $this->Form = new Form();
        $this->Form->fromJson($json);
    }

    /**
     * Determine if $field_name is a Column in the table this model models.
     *
     * @param  string  $field_name
     * @return bool
     */
    public function isColumn(string $field_name): bool
    {
        if (count($this->valid_columns) <= 0) {
            $this->getAllColumns();
        }

        if (isset($this->valid_columns[$field_name])) {
            return true;
        }

        return false;
    }

    /**
     * Validation of model, based on field requirements & table structure & extra rules.
     *
     * TODO: this could be re-written to use Form::isValid with extra rules injected and values set from Model
     *
     * @var bool
     */
    public function isFormValid(): bool
    {
        // Add required fields to field_rules
        $columns = $this->getAllColumns();

        $rules = [];
        foreach ($this->Form()->getFields() as $Field) {
            if (is_array($Field->validation_rules)) {
                $rules[$Field->getOriginalName()] = $Field->validation_rules;
            } else {
                $rules[$Field->getOriginalName()] = explode($this->multi_delimiter, $Field->validation_rules);
            }

            // We need to do this here because we're not using Form::isValid() we're using the values on the model itself
            // And validating against those using the Form rules + some extra from the table if possible
            if ($Field->attributes->required && ! in_array('required', $rules)) {
                $rules[$Field->getOriginalName()][] = 'required';
            }

            if (isset($columns[$Field->getOriginalName()])) {
                $this->addMaxRule($columns[$Field->getOriginalName()], $rules[$Field->getOriginalName()], $Field);
            }
        }

        // Set up the Validator
        $Validator = Validator::make(
            $this->getAttributes(),
            $rules
        );

        // Set error messages to fields
        if (! ($success = ! $Validator->fails())) {
            foreach ($Validator->errors()->toArray() as $field => $error) {
                $this->Form()->$field->error_message = current($error);
            }
        }

        return $success;
    }

    /**
     * Add or adjust max length rule if column exists and has a max length.
     */
    protected function addMaxRule($column, array &$rules)
    {
        // Set max length rule based on column length if available
        if (isset($column['length']) && $column['length'] != '') {
            // Find existing max: rules
            $max_rules = preg_grep('/^max:/', $rules);

            foreach ($max_rules as $key => $rule) {
                if ((int) substr($rule, 4) > $column['length']) {
                    $rules[$key] = 'max:'.$column['length'];
                }
            }

            if (count($max_rules) == 0) {
                $rules[] = 'max:'.$column['length'];
            }
        }
    }

    /**
     * Get a list of form data to build a form.
     *
     * @return void
     */
    protected function generateFormData(): void
    {
        $columns = $this->getAllColumns();

        foreach ($columns as $column) {
            $this->Form()->addField($column['name']);
            $this->Form()->{$column['name']}->attributes->maxlength = $column['length'];
            $this->Form()->{$column['name']}->default_value = $column['default'];
            $this->Form()->{$column['name']}->attributes->type = $this->getFormTypeFromColumnType($column['type']);
            if (is_array($column['values'])) {
                $this->Form()->{$column['name']}->options->setOptions($column['values']);
            }
            $this->Form()->addDisplayFields([$column['name']]);
        }
    }

    /**
     * Get a list of all valid columns on the model using this trait.
     *
     * @return array
     */
    protected function getAllColumns(): array
    {
        if (count($this->columns) > 0) {
            return $this->columns;
        }

        // If we have a MySQL Driver, then query directly to get Enum option values
        if (DB::connection()->getDriverName() == 'mysql') {
            $this->setColumnsFromMySQL();
        }
        // Otherwise query through Doctrine so we can get something still.
        else {
            $this->setColumnsFromOther();
        }

        return $this->columns;
    }

    /**
     * Set columns using data we can get through Doctrine.
     */
    private function setColumnsFromOther(): void
    {
        $columns = DB::connection()->getSchemaBuilder()->getColumnListing($this->table);

        foreach ($columns as $column_name) {
            $DoctrineColumn = DB::connection()->getDoctrineColumn($this->getTable(), $column_name);

            $this->columns[$column_name] = [
                'name' => $column_name,
                'type' => $DoctrineColumn->getType()->getName(),
                'default' => $DoctrineColumn->getDefault(),
                'length' => $DoctrineColumn->getLength(),
                'values' => null,
            ];
            $this->valid_columns[$column_name] = $column_name;
        }
    }

    /**
     * Get the field type based on column type.
     *
     * @param  string  $type
     * @return string
     */
    private function getFormTypeFromColumnType(string $type): string
    {
        switch ($type) {
            // TODO: Expand on this with more HTML5 field types
            case 'enum':
                return 'select';

            case 'text':
            case 'tinytext':
            case 'mediumtext':
            case 'longtext':
                return 'textarea';

            default:
                return 'text';
        }
    }
}