Adding the ability to create custom field types : Getting started

Currently, there is a decent set of core field types built into Laramanager. However, there might be a need for a custom field type specific to a project. So, let's see if I can put together a simple solution.

View Project

In Laramanager, field types are a list of input types for a form such as textarea, checkbox or even a text editor. Currently, there is no way to create custom field types without alterting the core. In this post, I'll take a look at Laramanager and see what needs to be done to allow the use of custom field types.

More information about field types can be found on the Fields and Field Types documentation pages.

To tackle this task of using custom field types, it is important to understand how Laramanager currently handles the core field types. Then try to figure out how to extract or extend these core processes.

Adding fields to resources

Creating or editing new resource fields is handled through the ResourceFieldController. Here is a portion of that controller.

PhilMareu/Laramanager/Http/Controllers/ResourceFieldController.php

class ResourceFieldController extends Controller {

    protected $fields = [
        'text' => 'Text',
        'email' => 'Email',
        'slug' => 'Slug',
        'password' => 'Password',
        'image' => 'Image',
        'images' => 'Images',
        'checkbox' => 'Checkbox',
        'textarea' => 'Textarea',
        'wysiwyg' => 'WYSIWYG',
        'select' => 'Select',
        'date' => 'Date',
        'relational' => 'Relational',
        'markdown' => 'Markdown'
//        'html' => 'HTML'
    ];

    ...

    public function create($resourceId)
    {
        $resource = $this->resource->find($resourceId);

        return view('laramanager::resources.fields.create', ['resource' => $resource, 'fields' => $this->getFields()]);
    }

  ...

    private function getFields()
    {
        return array_sort($this->fields);
    }

}

Notice that there is a hard coded list of field types. This list is used to populate the dropdown inside the create and edit forms.

Laramanager field type list dropdown

Clearly this list will need to be stored somewhere that is easily edited such as config file or database.

This controller also handles saving and updating a field for a resource. The field type slug is saved as the field's type. That might sound a little confusing but basically a resource field represents a form input. Form inputs have information such as title and validation. The "type" of form input could be "textarea". So this is what would be stored so Laramanager knows the "field type" for the input and then can render it appropriately.

This is important to note now, because later we'll see that there are hard coded references to the slug of the field type.

How entries use field types

An entry is a row of data from the resource's table. Field types are used to define an input field in an entry's form. This form is then used to insert or update a database row. For example, an "event" might have a title and description and look like this.

Resource Field Field Type Example Content
Events Title text Learn Laravel
Events Description textarea Visit us at Free State for beers and coding ...

The EntriesController handles CRUD operations for the entries.

Let's take a look at a portion of this class.

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

class EntriesController extends Controller
{
    protected $slug;

    protected $resource;

    protected $resourceRepository;

    protected $entriesRepository;

    public function __construct(Request $request, ResourceRepository $resourceRepository, EntriesRepository $entriesRepository)
    {
        $this->slug = $request->segment(2);
        $this->resource = $resourceRepository->getBySlug($this->slug);
        $this->resourceRepository = $resourceRepository;
        $this->entriesRepository = $entriesRepository;
    }

    ...

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

        return redirect('admin/' . $this->resource->slug)->with('success', 'Added');
    }

    ...
}

Let's take a closer look at the store method. First, validation is checked based on the the rules set when each field type was created. These rules are not properties of the field types so nothing to note here. Next the entity repository calls its create method. This is important because this method contains tasks that handle processing the data or any relationships are processes that are specific to each field type.

PhilMareu/Laramanager/Repositories/EntityRepository.php

class EntityRepository {

    ...

    public function create(Request $request, LaramanagerResource $resource)
    {
        $request = $this->processFields($request, $resource);
        $model = $this->getModel($resource);
        $entity = (new $model)->forceCreate($this->filterRequest($request, $resource));
        $this->processRelations($request, $resource, $entity);

        return $entity;
    }

    ...

    /**
     * @param $resource
     * @return string
     */
    private function getModel($resource)
    {
        return $resource->namespace . '\\' . $resource->model;
    }

    /**
     * @param Request $request
     * @param Resource $resource
     * @return Request
     */
    private function processFields(Request $request, LaramanagerResource $resource)
    {
        $fieldProcessor = new FieldProcessor($request, $resource);
        $request = $fieldProcessor->processAttributes();
        return $request;
    }

    private function processRelations(Request $request, LaramanagerResource $resource, $entity)
    {
        $relationProcessor = new RelationProcessor($request, $resource, $entity);
        $relationProcessor->processRelations();
    }

    private function filterRequest(Request $request, LaramanagerResource $resource)
    {
        return $request->except(array_merge(
            ['_token', '_method'],
            $resource->fields->where('type', 'images')->pluck('slug')->toArray()
        ));
    }

}

Ok, there is a lot going on here but the main thing to look at is the FieldProcessor and RelationProcessor classes. These are handling things that probably need to exist outside of the core for this update to work.

Field Processor

The FieldProcessor class will check each field and see if the data needs to be mutated before saving to the database.

class FieldProcessor {

    protected $request;

    protected $resource;

    public function __construct(Request $request, LaramanagerResource $resource)
    {
        $this->request = $request;
        $this->resource = $resource;
    }

    public function processAttributes()
    {
        foreach($this->resource->fields as $field)
        {
            if(method_exists($this, $field->type))
            {
                $this->{$field->type}($field->slug);
            }

        }

        return $this->request;
    }

    public function password($slug)
    {
        $value = $this->request->get($slug);

        if($value == "") $this->request->offsetUnset($slug);

        else $this->request->offsetSet($slug, bcrypt($value));
    }

    public function checkbox($slug)
    {
        if($this->request->has($slug)) $this->request->offsetSet($slug, 1);
        else $this->request->offsetSet($slug, 0);
    }
}

Now we see some of the hard coded slug names. The issue here is that in order to mutate data from a field type, it has to be added as a method in this core class. Clearly this will need to change.

Relation Processor

The RelationProcessor class checks if any of the field types are relationships and handles them appropriately.

src/PhilMareu/Laramanager/Fields/RelationProcessor.php

class RelationProcessor {

    protected $request;

    protected $entry;

    protected $resource;

    public function __construct(Request $request, LaramanagerResource $resource, $entry)
    {
        $this->request = $request;
        $this->entry = $entry;
        $this->resource = $resource;
    }

    public function processRelations()
    {
        foreach($this->resource->fields as $field)
        {
            if(method_exists($this, $field->type))
            {
                $this->{$field->type}($field);
            }

        }

        return $this->request;
    }

    public function images($field)
    {
        if($this->request->has($field->slug))
        {
            $entries = [];
            foreach($this->request->get($field->slug) as $key => $imageId)
            {
                $entries[$imageId] = ['ordinal' => $key];
            }

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

Again, this relies on hard coding and should be addressed in the new update. So, it sound like I need to have a way to accommodate these tasks as part of a field type.

How are field types rendered in the form

The last thing to review is how these field types are rendered. Field types have 4 views, "display", "field", "options" and "scripts". The display.blade.php view is the rendered preview of the input. This is mainly used to display the input in the admin panel. The field.blade.php view is the actual form input, options.blade.php show addition inputs related to the selected field type and the scripts.blade.php view contains any assets needed by the field type.

Here is an example of the "slug" field type views.

src/views/fields/slug/display.blade.php

{{ $entry->{$field->slug} }}

src/views/fields/slug/field.blade.php

@include('laramanager::partials.elements.form.slug', ['field' => ['name' => $field->slug, 'id' => 'slug', 'value' => isset($entry) ? $entry->{$field->slug} : null]])

src/views/fields/slug/options.blade.php

@include('laramanager::partials.elements.form.text', ['field' => ['name' => 'data[target]', 'label' => 'Field to slugify', 'value' => isset($field) ? unserialize($field->data)['target'] : '']])

src/views/fields/slug/scripts.blade.php

<script>
    var target = "{{ $field->data['target'] }}";

    $(function() {
        $('input[name="' + target + '"]').slugify({ slug: '#slug', type: "-" });
    });
</script>

Conclusion

So now that I have looked over the existing field type architecture, it is time to start working on the conversion. Basically the goal is to allow the system to use field types that are reference to a class anywhere in the project. I'll post as I go, so check back often or follow me on Twitter for updates.

2018 Phil Mareu - Coder, Traveler & Disc Thrower