Reusable UI/API CRUD Laravel and Angular

Posted: 2014-12-26 17:37:33

related repo

Working on a recent project it finally hit me how repetitive then need was to make a Resource at the API level and to make the UI to Create, Retrieve, Update, Delete, Index. And how easy it would be to make a foundation for this to use for each Resource keeping the UI goals consistent and simple between these Resources. With this came also a set of API tests in Behat that could also be used with the project.

What I will cover below is building out the foundation for this code and using it in a 'real life' example.

You will end up with a UI like this

Index

Simple search, create and edit

index

Modal Create

Simple Create leaving complexities for Edit

create

Modal Edit

edit

Now for the code

API

The API follows this pattern

Route

Build all the info into one namespace. For example using the a resource like the one in the images above call People and a name space of CoreApp.

CoreApp\PeopleApi

What is in here is (we will cover each in detail)

people

The route file then has

<?php


Route::get('people', 'CoreApp\People\PeopleController@getAll');
Route::get('people/{uuid}', 'CoreApp\People\PeopleController@getOne');
Route::post('people', 'CoreApp\People\PeopleController@postResource');
Route::put('people/{uuid}', 'CoreApp\People\PeopleController@putResource');

By using put and post Resource we all of this easier to reuse.

Then include this in your main route file for Laravel or register it with a provider.

#app/routes.php
require_once(__DIR__ . '/App/PeopleApi/routes.php');

So now the routes are in place lets go to the Controller

Controller

<?php

namespace App\People;


use App\Teams\Team; //some other model
use App\Users\User; //some other model we rely on 
use App\BaseController;
use App\People\PeopleService;//keep logic out of controller
use App\Services\ResponseServices; //just an example of a consistent response patter
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Response;
use Illuminate\Support\Facades\Validator;

class PeopleController extends BaseController {

    /**
     * @var PeopleService
     */
    private $peopleService;

    public function __construct(PeopleService $service, ResponseServices $responseServices)
    {

        $this->service = $peopleService; //@NOTE name it service not people service to ease the reuse.
        $this->responseServices = $responseServices;
    }

    /**
     * @Get('api/v1/people
     */
    public function getAll()
    {
        try
        {
            $results['people'] =  $this->service->getAll();
            return Response::json($this->responseServices->respond($results, "Loaded People"), 200);
        }
        catch(\Exception $e) {
            return Response::json($this->responseServices->respond($e->getMessage(), sprintf("Error Getting People Failed %s", $e->getMessage())), 422);
        }
    }

    /**
     * @Get('api/v1/people/{uuid}
     */
    public function getOne($uuid)
    {

        try
        {
            $results['person'] =  $this->service->getOne($uuid);
            if(!empty($results['person']))
            {
                $results['roles']  = Role::all();
                $results['teams']  = Team::all();
            }
            return Response::json($this->responseServices->respond($results, "Loaded People"), 200);
        }
        catch(\Exception $e) {
            return Response::json($this->responseServices->respond($e->getMessage(), sprintf("Error Getting People Failed %s", $e->getMessage())), 422);
        }
    }

    /**
     * @Post('api/v1/people')
     */
    public function postResource()
    {
        try
        {
            $store = $this->getInput();
        } catch(\Exception $e)
        {
            return Response::json($this->responseServices->respond($e->getMessage(), "PUT Failed"), 422);
        }

        $validator = Validator::make($store, $this->service->rulesCreate);

        if(!$validator->passes()) {
            return Response::json($this->responseServices->respond($validator->messages(), "Validation failed"), 422);
        }

        try
        {
            $results['person'] = $this->service->postPerson($store);
            /**
             * @TODO move Response to a class to not have it tightly coupled here
             */
            return Response::json($this->responseServices->respond($results, "Updated Team"), 200);
        }
        catch(\Exception $e) {
            return Response::json($this->responseServices->respond($e->getMessage(), sprintf("Error Updating Person %s", $e->getMessage())), 422);
        }
    }

    /**
     * @Put('api/v1/people/{uuid}
     */
    public function putResource($uuid)
    {
        try
        {
            $store = $this->getInput();
        } catch(\Exception $e)
        {
            return Response::json($this->responseServices->respond($e->getMessage(), "PUT Failed"), 422);
        }

        $validator = Validator::make($store, $this->service->rulesCreate);

        if(!$validator->passes()) {
            return Response::json($this->responseServices->respond($validator->messages(), "Validation failed"), 422);
        }

        try
        {
            $results = $this->service->updatePerson($store);
            /**
             * @TODO move Response to a class to not have it tightly coupled here
             */
            return Response::json($this->responseServices->respond($results, "Updated Person"), 200);
        }
        catch(\Exception $e) {
            return Response::json($this->responseServices->respond($e->getMessage(), sprintf("Error Updating Person %s", $e->getMessage())), 422);
        }
    }


} 

The Controller talks to the service to get the respond to the request and the consistently responds back using the responseService. (more on that shortly)

The Service

<?php

namespace CoreApp\People;

use CoreApp\Models\User;
use CoreApp\Services\UserService;
use CoreApp\Helpers\RoleHelper;
use CoreApp\Helpers\ThrowAndLogErrors;
use CoreApp\Services\BaseServices;
use CoreApp\Helpers\UuidHelper;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;

class PeopleService extends BaseServices {
    use RoleHelper;
    use ThrowAndLogErrors;
    use UuidHelper;

    /**
     * @var User
     */
    private $user;

    public $rulesCreate = [
        'email' => 'required|email'
    ];

    public function __construct(UserService $user)
    {
        $this->user = $user;
    }

    public function getAll()
    {
        try
        {
            $user_id = Auth::user()->id;
            if($this->isInRole($user_id, ['role-admin']))
            {
                $results = $this->user->all();
            } else {
                $results = [];
            }
        }
        catch(NotInRoleException $e)
        {
            $results = [];
        }
        catch(\Exception $e)
        {
            throw new \Exception("Error getting people message " . $e->getMessage());
        }
        return $results;
    }

    public function getOne($uuid)
    {
        try
        {
            if($this->isAdminRoleOrOwner($uuid))
            {
                $results = $this->user->getPerson($uuid);

            } else {
                $results = [];
            }
        }
        catch(\Exception $e)
        {
            throw new \Exception("Error getting people message " . $e->getMessage());
        }
        return $results;
    }



    public function updatePerson($store)
    {
        $this->isCurrentUserInRoleAdmin();
        try
        {
            $user = User::findOrFail($store['id']);
            $store = (array) $store;
            $user->email = $store['email'];
            (isset($store['image'])) ? $user->image = $store['image'] : false;
            $user->save();
            if(isset($store['roles']))
            {
                $user = $this->syncRoles($user, $store);
            }

        }
        catch(\Exception $e)
        {
            $message = sprintf("Error updating team %s", $e->getMessage());
            $this->throw_and_log_error($message);
        }
        return $user;
    }

    public function postPerson($store)
    {
        $this->isCurrentUserInRoleAdmin();
        $user = new User();

        try
        {
            /**
             * @TODO move this into a CoreApp service
             * I am moving away from the BehatEditor Service
             */
            $uuid = (!isset($store['id'])) ? $this->generateNewId()->toString() : $store['id'];
            $user->id = $uuid;
            $user->email = $store['email'];
            $user->password = Hash::make(Str::random(32));
            $user->image = $user->default_image;
            $user->save();
            $user = User::with('roles')->find($uuid); //due to uuid work
            if(isset($store['roles']))
            {
                $user = $this->syncRoles($user, $store);
            }


        }
        catch(\Exception $e)
        {
            $message = sprintf("Error creating user %s", $e->getMessage());
            $this->throw_and_log_error($message);
        }
        return $user;
    }

    protected function syncRoles($user, $store)
    {

        if(isset($store['roles']))
        {
            foreach($store['roles'] as $role)
            {
                $roles[] = $role['id'];
            }
            $user->roles()->sync($roles);
        }
        return $user;
    }


}