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');
}
...
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.
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.