carlosafonso/laravel-crud

View on GitHub
src/Controllers/CrudController.php

Summary

Maintainability
C
1 day
Test Coverage
<?php
namespace Afonso\LvCrud\Controllers;

use Afonso\LvCrud\Context;
use Afonso\LvCrud\Responses\ResponseBuilderFactory;
use Afonso\LvCrud\Models\CrudModelInterface;
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Support\Facades\Request as RequestFacade;
use Illuminate\Support\Facades\Response;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Pluralizer;
use RuntimeException;

abstract class CrudController extends BaseController
{
    /**
     * The model instance related to
     * this controller.
     *
     * @var Illuminate\Database\Eloquent\Model
     */
    protected $model;

    /**
     * The context of the current request.
     *
     * @var Afonso\LvCrud\Context
     */
    protected $ctx;

    /**
     * The response builder used to generate
     * content-type agnostic responses.
     *
     * @var Afonso\LvCrud\ResponseBuilderInterface
     */
    protected $response;

    /**
     * Whether this controller is read-only.
     *
     * Read-only controllers don't allow POSTs,
     * PUTs nor DELETEs.
     *
     * @var bool
     */
    protected $readOnly = false;

    public function __construct()
    {
        $this->model = $this->getRelatedModel();

        $this->ctx = new Context(RequestFacade::instance());

        $this->response = ResponseBuilderFactory::forRequest(RequestFacade::instance(), $this);
    }

    /*
     * CRUD methods
     */
    public function index()
    {
        $entities = $this->modifyIndexQuery($this->model->newQuery())
            ->with($this->ctx->with())
            ->paginate($this->ctx->pageSize());
        return $this->response->build($entities);
    }

    public function create()
    {
        return $this->response->build(null);
    }

    public function show($id)
    {
        $entity = $this->modifyShowQuery($this->model->newQuery())
            ->with($this->ctx->with())
            ->find($id);

        if (! $entity) {
            return $this->response->build(['error' => 'not_found'], SymfonyResponse::HTTP_NOT_FOUND);
        }

        // hook: afterShow()
        if ($hookResult = $this->afterShow($entity)) {
            return $hookResult;
        }

        return $this->response->build($entity);
    }

    public function store(Request $request)
    {
        if ($this->readOnly) {
            return $this->response->build(['error' => 'method_not_allowed'], SymfonyResponse::HTTP_METHOD_NOT_ALLOWED);
        }

        $data = $request->all();

        // hook: beforeValidate()
        if ($hookResult = $this->beforeValidate($request, $data)) {
            return $hookResult;
        }

        // hook: beforeStoreValidate()
        if ($hookResult = $this->beforeStoreValidate($request, $data)) {
            return $hookResult;
        }

        $validator = Validator::make($data, $this->model->getValidationRules());

        if ($validator->fails()) {
            return $this->response->build(
                ['error' => 'unprocessable_entity', 'validation_errors' => $validator->errors()->all()],
                SymfonyResponse::HTTP_UNPROCESSABLE_ENTITY
            );
        }

        // hook: beforeInsert()
        if ($hookResult = $this->beforeInsert($request, $data)) {
            return $hookResult;
        }

        $inserted = $this->model->create($data);
        $entity = $this->model->with($this->ctx->with())->find($inserted->id);

        // hook: afterInsert()
        if ($hookResult = $this->afterInsert($request, $entity)) {
            return $hookResult;
        }

        return $this->response->build($entity, SymfonyResponse::HTTP_CREATED);
    }

    public function edit($id)
    {
        $entity = $this->model->with($this->ctx->with())->find($id);
        if (! $entity) {
            return $this->response->build(['error' => 'not_found'], SymfonyResponse::HTTP_NOT_FOUND);
        }

        return $this->response->build($entity);
    }

    public function update(Request $request, $id)
    {
        if ($this->readOnly) {
            return $this->response->build(['error' => 'method_not_allowed'], SymfonyResponse::HTTP_METHOD_NOT_ALLOWED);
        }

        $entity = $this->model->find($id);
        if (! $entity) {
            return $this->response->build(['error' => 'not_found'], SymfonyResponse::HTTP_NOT_FOUND);
        }

        $data = $request->all();

        // hook: beforeValidate()
        if ($hookResult = $this->beforeValidate($request, $data)) {
            return $hookResult;
        }

        // hook: beforeUpdateValidate()
        if ($hookResult = $this->beforeUpdateValidate($request, $id, $data)) {
            return $hookResult;
        }

        $validator = Validator::make($data, $this->model->getValidationRules($entity->id));

        if ($validator->fails()) {
            return $this->response->build(
                ['error' => 'unprocessable_entity', 'validation_errors' => $validator->errors()->all()],
                SymfonyResponse::HTTP_UNPROCESSABLE_ENTITY
            );
        }

        // hook: beforeUpdate()
        if ($hookResult = $this->beforeUpdate($request, $data, $id)) {
            return $hookResult;
        }

        $entity->fill($data)->save();
        $entity = $this->model->with($this->ctx->with())->find($id);

        // hook: afterUpdate()
        if ($hookResult = $this->afterUpdate($request, $entity)) {
            return $hookResult;
        }

        return $this->response->build($entity, SymfonyResponse::HTTP_OK);
    }

    public function destroy($id)
    {
        if ($this->readOnly) {
            return $this->response->build(['error' => 'method_not_allowed'], SymfonyResponse::HTTP_METHOD_NOT_ALLOWED);
        }

        $entity = $this->model->find($id);
        if (! $entity) {
            return $this->response->build(['error' => 'not_found'], SymfonyResponse::HTTP_NOT_FOUND);
        }

        $entity->delete();

        return $this->response->build(null, SymfonyResponse::HTTP_NO_CONTENT);
    }
    /*
     * End of CRUD methods
     */

    /*
     * Hooks
     */

    /**
     * This hook runs after fetching a single
     * entity, as long as it exists.
     *
     * Returning a non-null value will cause the
     * controller method to automatically return
     * with that value.
     */
    public function afterShow($entity)
    {
        //
    }

    /**
     * This hook runs before validation occurs
     * both on insertions an updates.
     *
     * Returning a non-null value will cause the
     * controller method to automatically return
     * with that value.
     */
    public function beforeValidate(Request $request, &$data)
    {
        //
    }

    /**
     * This hook runs before validation occurs
     * on insertions, but after the beforeValidate()
     * hook has run.
     *
     * Returning a non-null value will cause the
     * controller method to automatically return
     * with that value.
     */
    public function beforeStoreValidate(Request $request, &$data)
    {
        //
    }

    /**
     * This hook runs before validation occurs
     * on updates, but after the beforeValidate()
     * hook has run.
     *
     * Returning a non-null value will cause the
     * controller method to automatically return
     * with that value.
     */
    public function beforeUpdateValidate(Request $request, $id, &$data)
    {
        //
    }

    /**
     * This hook runs before insertion occurs, but
     * after validation has passed.
     *
     * Returning a non-null value will cause the
     * controller method to automatically return
     * with that value.
     */
    public function beforeInsert(Request $request, &$data)
    {
        //
    }

    /**
     * This hook runs after insertion occurs, as long
     * as it succeeds.
     */
    public function afterInsert(Request $request, $entity)
    {
        //
    }

    /**
     * This hook runs before updating occurs, but
     * after validation has passed.
     *
     * Returning a non-null value will cause the
     * controller method to automatically return
     * with that value.
     */
    public function beforeUpdate(Request $request, &$data, $id)
    {
        //
    }

    /**
     * This hook runs after updating occurs, as long
     * as it succeeds.
     */
    public function afterUpdate(Request $request, $entity)
    {
        //
    }

    /**
     * Returns a new query builder instance,
     * optionally based on the instance received
     * as a parameter, to be used in the query
     * that fetches a list of resources (index).
     */
    public function modifyIndexQuery(Builder $q)
    {
        return $q;
    }

    /**
     * Returns a new query builder instance,
     * optionally based on the instance received
     * as a parameter, to be used in the query
     * that fetches a single resources (show).
     */
    public function modifyShowQuery(Builder $q)
    {
        return $q;
    }

    /*
     * End of hooks
     */

    /**
     * Returns an instance of the model
     * related to this controller. If the model
     * does not implement the CrudModelInterface
     * it will rise an exception.
     *
     * @throws RuntimeException if the related model does
     * not implement the CrudModelInterface.
     *
     * @return  Illuminate\Database\Eloquent\Model
     */
    public function getRelatedModel()
    {
        $class = $this->getRelatedModelClass();
        $instance = new $class;
        if ($instance instanceof CrudModelInterface) {
            return $instance;
        }

        throw new RuntimeException("The related model must implement CrudModelInterface");
    }

    /**
     * Returns the fully qualified class
     * of the model associated with this
     * controller.
     *
     * This method assumes that the model
     * class is the singularized form of
     * the controller's name minus the
     * 'Controller' suffix.
     *
     * It will also assume that the model class
     * is namespaced under 'App', unless specified
     * otherwise. A different namespace can
     * be set by overriding getModelNamespace().
     *
     * @return  string
     */
    public function getRelatedModelClass()
    {
        $fqController = explode('\\', static::class);
        $model = Pluralizer::singular(str_replace('Controller', '', end($fqController)));

        $ns = $this->getModelNamespace() ? : 'App';
        return $ns . '\\' . $model;
    }

    /**
     * Returns the specific namespace of the
     * model related to this controller.
     *
     * Override this function if your model does
     * not follow the namespacing conventions used
     * by this controller.
     *
     * @return  string
     */
    protected function getModelNamespace()
    {
        return null;
    }

    /**
     * Returns whether this controller instance
     * supports JSON responses.
     *
     * @return  boolean
     */
    public function supportsJson()
    {
        return true;
    }

    /**
     * Returns whether this controller instance
     * supports HTML responses.
     *
     * @return  boolean
     */
    public function supportsHtml()
    {
        return true;
    }
}