Overview::
I have a Restful API that pushes all /api/{version}/{Repository}
into my ResourceV{1}Controller
depending on the {version}
number. That Controller
extends my Abstract Controller that has the majority of the controller logic, the ResourceV1Controller would load the V1 Interface for the API, and ResourceV2Controller
would load V2 interface for the API etc.
I use a factory to load the correct API based on the {Repository}
value to inject into my Controller.
I also inject the correct Entity into my Repositories when they are injected in my controller, so in the Repository I can reference School::Where(....)->get()
as $this->entity->where(...)->get()
instead.
This is two fold, I want to be able to write tests for each of these Repositories, and I want to use a Trait that moves duplicated code across all Repositories into one Location.
Here is what I have:
Controller::
<?php namespace App\Http\Controllers;
use App\Api\Resource\v1\ResourceAPIInterface;
use Illuminate\Http\Request;
use Illuminate\Routing\Router;
/**
* Class ResourceController
*/
class ResourceV1Controller extends RestController
{
/**
* @param ResourceAPIInterface $api
* @param Request $request
* @param Router $router
*/
public function __construct(ResourceAPIInterface $api, Request $request, Router $router)
{
$this->api = $api;
parent::__construct($request, $router);
}
}
Parent Controller::
<?php namespace App\Http\Controllers;
use App\Api\Resource\v1\ResourceAPIInterface;
use Illuminate\Http\Response;
use Illuminate\Http\Request;
use Illuminate\Routing\Router;
/**
* Class RestController
* @package App\Http\Controllers
*/
abstract class RestController extends Controller
{
/**
* @var
*/
public $api;
/**
* @param Request $request
* @param Router $router
*/
public function __construct(Request $request, Router $router)
{
$this->request = $request;
}
/**
* Display a listing of the resource.
* @return \Illuminate\Http\Response
*/
public function index($version, $resource)
{
$input = $this->request->all();
$data = $this->api->fetchAll($input);
if(empty($data)){
return (new Response(" ", 204))->header('Content-Type', 'application/json')->header('Cache-Control', 'max-age=3600');
}else {
return (new Response($data, 200))->header('Content-Type', 'application/json')->header('Cache-Control', 'max-age=3600');
}
}
}
IoC Binding in Service Provider::
The $uri variable is exploded from the global REQUEST_URI variable
$this->app->bind('App\api\Resource\v2\ResourceAPIInterface', function() use ($uri){
$parts = explode('-', strtolower($uri[3]));
$parts = array_map('ucfirst', $parts);
$resource = implode('', $parts);
$version = $uri[2];
return Factory::getInstance($resource, $version);
});
$this->app->bind('App\api\Entity\v1\EntityInterface', function() use ($uri){
$parts = explode('-', strtolower($uri[3]));
$parts = array_map('ucfirst', $parts);
$resource = implode('', $parts);
$class = 'App\api\Entity\v1\\'.$resource;
return new $class;
});
Factory::
<?php namespace App\api;
/**
* API Factory creates the correct API/version to inject into controller
*
* @author Dustin Parham <[email protected]>
*/
Class Factory
{
const NS = "App\\api\\";
/**
*
* @param string $api
* @param int $version
* @return APIInterface;
*/
public static function getInstance($api, $version='current')
{
if($version == 'current'){
$apiConfig = \Config::get('api');
//Check if API resource exists
if(isset($apiConfig[$api])){
$version = $apiConfig[$api]['currentVersion'];
}else {
//Need to throw 404 error
\App::abort(404);
}
$version = $apiConfig[$api]['currentVersion'];
}else {
$apiConfig = \Config::get('api');
$arr1 = str_split($version);
//Check if API resource exists
if(isset($apiConfig[$api])){
$arr2 = str_split($apiConfig[$api]['currentVersion']);
}else {
//Need to throw 404 error
\App::abort(404);
}
if((int)$arr1[1] > (int)$arr2[1]){
$version = $apiConfig[$api]['currentVersion'];
}
}
$apiClass = self::NS.$api."\\".$version."\\".$api.'API';
if (class_exists($apiClass)) {
return \App::make($apiClass);
}else {
//Need to throw 404 error
\App::abort(404);
}
}
}
The config file api is an array of Repositories, and what their current version is.
Example Repository::
<?php namespace App\api\School\v1;
use App\api\Resource\v1\ResourceAPIInterface;
use App\api\Entity\v1\EntityInterface;
/**
* Class SchoolAPI
* @package App\api\School\v1
*/
class SchoolAPI implements ResourceAPIInterface
{
const VERSION = '1';
const NAME='School';
public $entity;
/**
* @return string
*/
public function version()
{
return self::VERSION;
}
/**
* @return string
*/
public function name()
{
return self::NAME;
}
/**
* @return EntityInterface
*/
public function getEntity()
{
return $this->entity;
}
/**
* @param EntityInterface $entityInterface
*/
public function __construct(EntityInterface $entityInterface){
$this->entity = $entityInterface;
}
/**
* @param $data
* @return \Illuminate\Database\Eloquent\Collection
*/
public function fetchAll($data)
{
$data = $this->entity->all();
return $data;
}
...
Trait::
<?php namespace App\api\Traits\v1;
use App\api\Entity\v1\EntityInterface;
trait ApiUtilities {
/**
* @return EntityInterface
*/
abstract public function getEntity();
/**
* @param $id
* @return \Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Eloquent\Model
*/
public function deleteOne($id)
{
$data = $this->getEntity()->find($id);
return $data->delete();
}
/**
* @param $id
* @param string $fieldName
* @return bool
*/
public function toggle($id, $fieldName='active')
{
$item = $this->getEntity()->find($id);
$item->{$fieldName} = (int) !$item->{$fieldName};
return $item->save();
}
}
Overall, Is this the best way to accomplish this? I've seen different approaches to dealing with versioning a RESTful API, but wanted to cut down on duplicated code, and excess files.
ResourceV1Controller
. – Ben Harold Feb 25 '15 at 17:03