Laravel Form Refactoring with Service layers

laravel form refactoring with service layers

Update September 2017: This article does not implement any custom Namespace, in time it wil be updated to include more modern Laravel coding techniques.

After writing my last article on Developing Applications with Laravel – Part 3 I started on part 4 and I took a look back at early controller code I had written and realised there must be a better way to validate form data and then call the lower level model code that saves the data back to the database.

After some investigation I concluded that using both the Request classes and a Service Layer implementation would be a nice way to then use the Eloquent code we coded in the model classes in the previous article. So rather than just jump into part 4, I decided this shorter article would be a nice fit and provide a useful working implementation on the topic of Request and Service Layers.

As I delve more and more into Laravel I find myself constantly refactoring code into smaller more manageable and more reusable pieces. One area this has been beneficial has been in my business logic where I handle forms and save data back to the database.

In this article I am going to refactor some old code to use Request Validation and add a Service layer. Request Validation files can be found in /app/Http/Requests, in a fresh application you will not have any files until you manually create them.

Using the artisan tool, I have created each Request Validation class, a sample command follows:

php artisan make:request WorkFlowRequest

This will create a templated Request class in the directory mentioned above. The class comes with two methods by default, authorize() and rules().

For testing I edited the authorize() method to return "true;" rather than the default "false;",  this enables authentication when called. If you need custom authentication handling then this is where you put it and I will touch on that in another article. If authorize() returns false then a post of the form will display “FORBIDDEN” in the top corner of the web browser.

The rules() method is where we return an array of validation rules. The following are an example:

public function rules()
{
return [
    'workflow_name'=>'required|min:5|max:128',
    'workflow_description'=>'required|min:5|max:1024',
    'workflow_aid'=>'required',
    'workflow_create_date'=>'required',
    'workflow_status'=>'required'
    ];
}

If the completed form meets the validation then the controller code is executed for the given route provided the class is defined in the method call as shown below:

Before changes to Controller method:

public function SaveWorkflow($id)

After addition of Request Validation:

public function SaveWorkflow(WorkFlowRequest $request, $id)

There is no change to the route logic to include the request class.

In addition to the changes to the standard templated class file outlined above, I also added a messages() function so I can return failure messages back to the form automatically. The messages() method is not defined by the artisan “make” command so you need to manually add it. The method is simple enough:

public function messages()
{
    return [
        'workflow_name.required'=>'The workflow name is required',
        'workflow_name.min'=>'The workflow name must be at least 5 characters minimum',
        'workflow_name.max'=>'The workflow name must be between 5 and 128 characters',
        'workflow_description.required'=>'A description is required (>5 and < 1025 characters)',
        'workflow_description.min'=>'Description must be 5 or more characters',
        'workflow_description.max'=>'Description must be less than 1025 characters',
        'workflow_status.required'=>'Status field is required'
        ];
}

The complete Request file now looks like:

<?php namespace AppHttpRequests;

use AppHttpRequestsRequest;

class WorkFlowRequest extends Request
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'workflow_name'=>'required|min:5|max:128',
            'workflow_description'=>'required|min:5|max:1024',
            'workflow_aid'=>'required',
            'workflow_create_date'=>'required',
            'workflow_status'=>'required'
        ];
    }


    public function messages()
    {
        return [
            'workflow_name.required'=>'The workflow name is required',
            'workflow_name.min'=>'The workflow name must be at least 5 characters minimum',
            'workflow_name.max'=>'The workflow name must be between 5 and 128 characters',
            'workflow_description.required'=>'A description is required (>5 and < 1025 characters)',
            'workflow_description.min'=>'Description must be 5 or more characters',
            'workflow_description.max'=>'Description must be less than 1025 characters',
            'workflow_status.required'=>'Status field is required'
        ];
    }

Controller Code

Open the relevant controller file (for this example its called WorkflowController.php) and locate all the code that checks the validity of fields during insert and update operations, there may only be two places, an initial save method such as SaveWorkflow() and an UpdateWorkflow() method when you edit and save the form data.

Lets look at the original method before changes:

public function SaveWorkflow($id)
{
    $WF = new AppWorkflows();
    $form = Input::all();
    $name = $form['workflow_name'];
    $desc = $form['workflow_description'];
    $aid = $form['workflow_aid'];
    $status =$form['workflow_status'];
    $crdate =$form['workflow_create_date'];
    if(strlen($name)>8)
    {
        if($id == $form['id'])
        {
            $data = array(
                'workflow_name'=>$name,
                'workflow_description'=>$desc,
                'workflow_aid'=>$aid,
                'workflow_status'=>$status,
                'workflow_create_date'=>$crdate);
            $rv = $WF->UpdateWorkflow($id,$data);
            if($rv==1)
                Session::flash('flash_message','Workflow updated successfully!');
            if($rv==0)
                Session::flash('flash_error','No Workflow update needed!');
        }
        else
            Session::flash('flash_error','ERROR - Invalid Workflow record.');
    }
    else
        Session::flash('flash_error','ERROR - Workflow name is too short (more than 8 characters please).');
    return $this->ShowWorkflowList();
}

The original method does both the crude validation and saves the data back to the database via the Model code. It's acceptable but we can now refactor it to slim down our code. First, change the function signature to include the Request class, next remove all code that checks the fields and their lengths, that's now done for us in the Request validation rules() method.

The slimmed down code before we add a Service layer looks like this:

public function UpdateWorkflow(WorkFlowRequest $request,$id)
{
    $WF = new AppWorkflows();
    $data = array(
        'workflow_name'=>$request['workflow_name'],
        'workflow_description'=>$request['workflow_description'],
        'workflow_aid'=>$request['workflow_aid'],
        'workflow_status'=>$request['workflow_status'],
        'workflow_create_date'=>$request['workflow_create_date']);
    if($WF->UpdateWorkflow($id, $data) == 1)
    {
        Session::flash('flash_message','Workflow updated successfully!');
    }
    return $this->ShowWorkflowList();
}

We can shorten it even furthur IF the form field names match the database column names exactly to:

$data = $request->except(['_token'])

Already we have reduced the code significantly and made the framework do a lot of the work for us, as a bonus, our validation is in one place (the Requests directory) so additional fields can be changed in the validation logic without impacting any methods that use it.

However, there are two issues with this code, in this example it's duplicated for two methods (SaveXXXX() and UpdateXXXX() ) so we do have the potential of incorrectly saving some fields and not others if we do modifications. We also can't see our validation errors in the View when the validation fails, so we need to implement that facility as well. Our first step of the refactor is completed.

Capturing Errors in the View

Inside every form handling view, you can display the form validation errors using the following code. It first checks how many errors and returns a list. This code does not highlight the field that is at fault so you can add to it to do that later.

@if(count($errors)>0 )
    <div class="alert alert-danger col-xs-4">
        <ul>
        @foreach($errors->all() as $error)
            <li>{{ $error }}</li>
        @endforeach
        </ul>
    </div>
@endif

If there are no errors then this code does not render anything in the view, so an initial load of a “Save New” form will have no error list and only a blank form shown as expected. When we display the edit form, there will be no issues so the view renders accordingly. The Validation logic will fill the $errors array with the list of fields that failed validation in the Request class, so these will be listed in a unordered bullet list in a red div in our Bootstrap application framework.

The Service Layer

When you map out the directory structure of Laravel 5.2 you find there is no “Services” folder. Don't confuse this with ServiceProviders which are a different topic. To solve this we will add a new directory, and in this example it will be under the “app/Models” directory. We also need to map it in our composer.json file IF we don't use a separate namespace, if you use the App namespace then you need to include these mappings:

"autoload": {
    "classmap": [
        "database",
        "app/Models",
        "app/Models/Services",
        "app/Tasks"
        ],
        "psr-4": {
            "App\": "app/"
            }
    },

Now we go to our Models directory and create the Services directory.

# cd app/Models
# mkdir Services 
# cd ../..
# composer dump-autoload

By now doing a "composer dump-autoload", we will configure our project with the new directory. We can now add a Service class to handle saving our data back.

There is no Artisan tool to make the Services class, so we can change into the directory and create the file manually. The WorkFlowService class will have two public methods, insert() and update(). This will take the data from our request validation and persist it to our database. It provides a single place where both operations are performed and called from anywhere in the framework. The class file looks like:

<?php namespace App;

use AppHttpRequestsWorkFlowRequest;



class WorkFlowService
{

    public static function insert(WorkFlowRequest $request)
    {
        $WF = new AppWorkflows();
        if($WF->InsertWorkflow( $request) > 0)
        {
            Session::flash('flash_message','Workflow Saved!');
        }
    }




    public static function update(WorkFlowRequest $request)
    {
        $WF = new AppWorkflows();
        if($WF->UpdateWorkflow($request['id'],$request) == 1)
        {
            Session::flash('flash_message','Workflow updated successfully!');
        }
    }
}

Inside our controller we now change the previously refactored method yet again, this time to use the service layer:

public function SaveWorkflow(WorkFlowRequest $request,$id)
{
    AppWorkFlowService::update($request);
    return $this->ShowWorkflowList();
}

At the top of our controller include a use statement for the Service class.

use AppHttpRequestsWorkFlowRequest;
use AppModelsServicesWorkFlowService;

This exercise has considerably reduced our controller code, it has also placed our insert and update code into a single file where it can be maintained much easier with virtually no gremlins coming in as a result of column changes.

Now you can refactor all methods that handle form code and save considerable space.

-oOo-

Back to the Blog

avatar of sid young

Sid Young

  • Conetix
  • Conetix

Let's Get Started

  • This field is for validation purposes and should be left unchanged.