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.
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.
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.