Adding the ability to create custom field types: The field type list

This is the final post in my custom field type series. I'll create the field type classes and get them working with the current code base.

View Project

Now it is time to create the actually field type classes. I believe the easiest one to start with is the "text" field.

src/PhilMareu/Laramanager/FieldTypes/TextFieldType.php

class TextFieldType
{

}

First, I'll use the resource "Conference Events" and try to add a text field. When I save the field, I expect an error since the code to add the field as a relation doesn't exist yet.

Illuminate \ Database \ QueryException (42S22)
SQLSTATE[42S22]: Column not found: 1054 Unknown column 'type' in 'field list' (SQL: insert into `laramanager_resource_fields` (`title`, `slug`, `validation`, `is_unique`, `list`, `type`, `resource_id`, `updated_at`, `created_at`) values (Title, title, required, 1, 1, 1, 1, 2018-07-30 14:09:26, 2018-07-30 14:09:26))

I'll fix this by saving the relation. First, I'll remove the "type" from the list of fillable fields. It doesn't exist anymore.

src/PhilMareu/Laramanager/Models/LaramanagerResourceField.php

class LaramanagerResourceField extends Model {

    protected $fillable = [
        'title',
        'slug',
        'validation',
        'is_required',
        'is_unique',
        'data',
        'list'
    ];

    ...
}

Then save the relation to the newly created field.

class ResourceFieldController extends Controller {

    ...

    /**
     * Store a newly created resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request, $resourceId)
    {
        $resource = $this->resource->find($resourceId);

        $this->validate($request, [
            'title' => 'required|max:255',
            'slug' => 'required|max:255',
            'validation' => 'required',
            'is_unique' => 'boolean',
            'is_required' => 'boolean',
            'field_type_id' => 'required|in:' . $this->fieldTypes->implode('id', ','),
            'data' => 'array'
        ]);

        $attributes = $this->serializeData($request);

        $resource = $resource->fields()->make($attributes);
        $resource->fieldType()->associate(
            $this->fieldTypes->where('id', $request->field_type_id)->first()
        );
        $resource->save();

        return redirect('admin/resources/' . $resourceId . '/fields')->with('success', 'Field added');
    }

    ...
Check out more updates on Github.

When selecting a certain field type, there is an AJAX call to retrieve any options. Currently, it looks for a view that is basically hard coded in a method. It needs to be updated to handle it dynamically. What I want is a simple way to ask for the view. I've decided to store the view directory location in the database with the field type and then just use the class to process the data specific to the type.

Check out this update on Github.

I'll make a couple helpers on the model to make grabbing views a bit easier.

class LaramanagerFieldType extends Model
{
    ...

    public function getOptionView()
    {
        if(view()->exists($this->getViewPath('options'))) return view($this->getViewPath('options'))->render();

        return '';
    }

    public function getViewPath($name)
    {
        return "{$this->views}.{$this->slug}.$name";
    }
}

Now to update the response method for the options call.

src/PhilMareu/Laramanager/Http/Controllers/ResourceFieldController.php

public function getOptions($fieldTypeId)
{
    $fieldType = $this->fieldTypes->where('id', $fieldTypeId)->first();

    return response()->json(['data' => ['html' => $fieldType->getOptionView()]]);
}

This works great, so I'll move on to displaying field values.

Displaying the field

The display view part of the field type shows the admin the value of the field. In some cases, like a relation, it helps to eager load the relation data. But before I go to far, I think this is a great time to create an abstract field type class that should always be extended by the developer.

src/PhilMareu/Laramanager/FieldTypes/FieldType.php

abstract class FieldType
{
    /**
     * The name of the relationship to eager load.
     *
     * @return array
     */
    public function eagerLoad()
    {
        return [];
    }
}

The eagerLoad method will return an array of relationship names that should be eager loaded. Let's update the EntriesRepository to use this and remove the hard code.

src/PhilMareu/Laramanager/Repositories/EntriesRepository.php

$eagerLoad = $resource->listedFields->map(function($field) {
            return $field->fieldType->getClass()->eagerLoad();
        })->flatten()->all();

To make this work, I had to add a method in the model for instantiating the field type class.

src/PhilMareu/Laramanager/Models/LaramanagerFieldType.php

class LaramanagerFieldType extends Model
{
    ...

    public function getClass()
    {
        return (new $this->class);
    }

    ...
}

Adding field content

When displaying a field type for an entry form, it might need to load or process data. For example, the select field needs to parse the options and the relational field needs to grab the relation list. Let's take a look at the current process.

/src/PhilMareu/Laramanager/Http/Controllers/EntriesController.php

public function create()
{
    $options = $this->resource->fields->filter(function($field) {
        return $field->type == 'relational';
    })->reduce(function($options, $field) {
        return array_merge($options, [$field->slug => $this->entriesRepository->getFieldOptions($field)]);
    }, []);

    return view('laramanager::entries.create')
        ->with('resource', $this->resource)
        ->with('options', $options);
}

I think the options variable is too specific. Any resources that the field needs will be done in the field itself. For example, instead of the controller sending a list to the view for a select field, it should be done in the view with custom methods on the field type.

Data mutations

When saving data, some fields need their data mutated. For example, the password field will probably need to encrypt the value. I'll add a method to the field type class and implement it in the repository.

src/PhilMareu/Laramanager/FieldTypes/PasswordFieldType.php

class PasswordFieldType extends FieldType
{
    /**
     * @param Request $request
     * @param $name
     * @return Request
     */
    public function mutate(Request $request, $name)
    {
        if($request->filled($name)) $request->offsetSet($name, bcrypt($request->get($name)));

        else $request->offsetUnset($name);

        return $request;
    }
}

src/PhilMareu/Laramanager/Repositories/EntriesRepository.php

/**
 * @param Request $request
 * @param Resource $resource
 * @return Request
 */
private function processFields(Request $request, LaramanagerResource $resource)
{
    foreach($resource->fields as $field)
    {
        $request = $field->fieldType->getClass()->mutate($request, $field->slug);
    }

    return $request;
}

Saving relations

Some fields might need to save a relation. The "Images" field is a good example.

src/PhilMareu/Laramanager/FieldTypes/ImagesFieldType.php

class ImagesFieldType extends FieldType
{
    public function relations(Request $request, $field, $entry)
    {
        if($request->filled($field->slug))
        {
            $entries = [];
            foreach($request->get($field->slug) as $key => $imageId)
            {
                $entries[$imageId] = ['ordinal' => $key];
            }

            $entry->{$field->data['method']}()->sync($entries);
        }
    }
}

I'll update the repository to iterate through the fields and call this method.

src/PhilMareu/Laramanager/Repositories/EntriesRepository.php


private function processRelations(Request $request, LaramanagerResource $resource, $entry)
    {
        foreach($resource->fields as $field)
        {
            $field->fieldType->getClass()->relations($request, $field, $entry);
        }
    }

Conclusion

There is a few more things to clean up but it is basically ready to go. Now instead of updating the core with new fields, I can develop separate field types that will just work with Laramanager.

2018 Phil Mareu - Coder, Traveler & Disc Thrower