I’ve recently discovered that presenters (like this one) implement the decorator pattern and are a great way to add fields and logic to existing Laravel models. Take the following example for my question below:
// Tack on a new readable timestamp field.
public function timeago()
{
return $this->object->created_at->whenForHumans();
}
// Wrap an existing field with some formatting logic
public function created_at()
{
return $this->object->created_at->format('Y-m-d');
}
I can then use these presenter fields in my view:
{{ $object->timeago }}
{{ $object->created_at }}
How would you implement the decorator pattern for an API that returns JSON responses rather than Blade views? In all the Laravel/JSON articles I have read, objects are immediately returned without undergoing any transformation / presenter logic. e.g.:
// converting a model to JSON
return User::find($id)->toJson();
// returning a model directly will be converted to JSON
return User::all();
// return associated models
return User::find($id)->load('comments')->get();
How can I implement presenter fields in my JSON response?
$object->timeago
$object->created_at
As you mentioned, User::all returns JSON, so do something like:
Some function to get data and return a decorated response:
public function index()
{
$news = News::all();
return $this->respond([
'data' => $this->newsTransformer->transformCollection($news->toArray())
]
);
}
The above function will call Transformer::transformCollection:
<?php namespace Blah\Transformers;
abstract class Transformer {
public function transformCollection(array $items)
{
return array_map([$this, 'transform'], $items);
}
public abstract function transform($item);
}
which in turn will call NewsTransformer::transform():
public function transform($news)
{
return [
'title' => $news['title'],
'body' => $news['body'],
'active' => (boolean) $news['some_bool'],
'timeago' => // Human readable
'created_at' => // Y-m-d
];
}
The end result being JSON with the format you require, in this case:
{
data: {
title: "Some title",
body: "Some body...",
active: true,
timeago: "On Saturday, 1st of March",
created_at: "2014-03-01"
}
}
By the way, Laracasts has an excellent series on building APIs -- hope that helps!
For clarity, the respond function in the first code snippet just wraps the data with a status code, and any headers, something like:
return Response::json($data, 200);
Related
I am currently doing a small project to learn some laravel validation and ran into a problem.
The API endpoint is api/test/schoolbook?start= and my validation is
'start' => ['date_format:Y-m-d H:i:s']
While this works like a charm and sorts the schoolbooks by a start year, i think my validation has some error. It validates if start is equal to the defined date format, all good. but if i now parse just ?start=without any thing, it still goes through, but doesn't throw an error message (it just returns everything without sorting)
Is there a way i can validate this better and prevent the query string parameter to be empty?
If start is not passed, it should return all the records, so i cant make it required really.
So the scenarios are:
?start=date is passed in the right format and returns all the schoolbooks by the passed date,
?start=date is not passed and returns all the records in the database
?start= should also return 'has to be in date format validation'
Thank you!
The Controller:
public function findSchoolbook(
SchoolBookRequest $request,
) : JsonResponse {
$schoolbook = $this->schoolkbool->sort($paramBag);
$response = $this->transformer()->paginator($schoolbook);
return $this->response($response);
}
The ParamBg method i use
private function getParamBag(SchoolBookRequest $request) : ParamBag
{
return ParamBag::create()
->setPage($request->get('page'))
->setPerPage($request->get('per_page'))
->setStartDate($request->get('start_at'))
}
The Request
class SchoolBookRequest extends Request
{
public function rules() : array
{
return [
'start_at' => ['date_format:Y-m-d H:i:s']
];
}
}
This should only filter in start date when present in the request:
private function getParamBag(SchoolBookRequest $request) : ParamBag
{
$parambag = ParamBag::create()
->setPage($request->get('page'))
->setPerPage($request->get('per_page'))
if ($request->has('start_at')) {
$parambag = $parambag->setStartDate($request->get('start_at'));
}
return $parambag;
}
I am creating REST API in YII2. I am getting all post data by calling get API
/post/
/post/1/
But I want to get user also who post that particular post.
for example I want data in below format
{
"id":"1",
"title":"kapil",
"content" : "test",
"user" : {
"username":"admin",
"first_name":"kapil",
"last_name":"sharma",
//blah blah
}
}
But response is
{
"id":"1",
"title":"kapil",
"content" : "test",
}
I used this tutorial for creating API.
Let's say in your post method you have the getIdUser() relation:
public function getIdUser() {
return $this->hasOne(User::className(), ['id' => 'user_id']);
}
In that model, you should make use of the extraFields() method, as follows:
public function extraFields() {
return [
'user' => 'idUser' // or the name you hasOne relation with user has
];
}
Then, you call your REST API with the expand parameter, specifying there which extraField details you'd like to include, in your case:
http://example.com/post/view?id=1&expand=user
I'm creating a RESTful API with Yii2 and have successfully setup a model named Contacts by following the Quick Start Tutorial*. I love how records can be created, listed, updated and deleted without creating any actions.
However I can't see how to filter results. I would like to only return contacts where contact.user_id is equal to 1 (for example) as it currently will reply with all records. Is this possible without creating the actions?
I am unsure also how I can limit results. From what I've read I feel it should append the URI with ?limit=5.
http://www.yiiframework.com/doc-2.0/guide-rest-quick-start.html
You should return a dataprovider instead of a set of objects, that supports pagination for you.
Perhaps this approach will be a bit more useful:
public function actionIndex()
{
return new \yii\data\ActiveDataProvider([
'query' => Contact::find()->where(['user_id' => \Yii::$app->user-id]),
]);
}
You could also leave the index action intact, but provide the preset action with a prepareDataProvider-callback:
public function actions()
{
$actions = parent::actions();
$actions['index']['prepareDataProvider'] = function($action)
{
return new \yii\data\ActiveDataProvider([
'query' => Contact::find()->where(['user_id' => \Yii::$app->user-id]),
]);
};
return $actions;
}
Hope that helps.
I have had to override the index method despite not wanting to. My solution looks like this:
public function actions()
{
$actions = parent::actions();
unset($actions['index']);
return $actions;
}
public function actionIndex()
{
return Contact::findAll(['user_id' => \Yii::$app()->user-id]);
}
I guess this solution means I need to write my own pagination code however which is something else I was hoping to avoid.
I have a tv show netflix-esque project I'm building where I have a Shows page which I want to filter on format. Each show contains episodes which can have a tv, dvd and bd format.
Currently I'm filtering using separate routes and controllers which extend the base ShowsController.
Route::get('shows/view/{type}', ['as' => 'shows.viewtype', 'uses' => 'ShowsController#viewType',]);
Route::get('shows/bluray',['as' => 'shows.bluray','uses' => 'ShowsBlurayController#index']);
Route::get('shows/dvd',['as' => 'shows.dvd','uses' => 'ShowsDVDController#index']);
Route::get('shows/tv',['as' => 'shows.tv','uses' => 'ShowsTVController#index']);
Example of one of the format controllers
class ShowsBlurayController extends ShowsController
{
public function index()
{
// Set user state for browsing bluray
Session::push('user.showtype', 'bluray');
$shows = $this->show->getBlurayPaginated(16);
return $this->getIndexView(compact('shows'));
}
}
I use the getIndexView() method (in the ShowsController) to determine one of 2 available views: poster and list.
public function getIndexView($shows)
{
$viewType = get_session_or_cookie('show_viewtype', 'list');
if ($viewType == 'posters') {
return View::make('shows.index', $shows)
->nest('showsView', 'shows.partials.posters', $shows);
} else {
return View::make('shows.index', $shows)
->nest('showsView', 'shows.partials.list', $shows);
}
}
The shows are filtered based on the episodes:
public function getBlurayPaginated($perPage)
{
return $this->getByFormat('BD')->with('tagged')->paginate($perPage);
}
private function getByFormat($format)
{
return $this->show->whereHas('episodes', function ($q) use ($format) {
$q->whereHas('format', function ($q) use ($format) {
$q->where('format', '=', $format);
});
});
}
The problem is that I want to do this in a clean way. When a user selects a format, that filter will be applied. Currently all of this is kind of scattered across controllers and doesn't quite make sense.
I also thought of doing something like this in the routes.php:
Route::get('shows/format/{format}',['as' => 'shows.format','uses' => 'ShowsController#index']);
And then handle the filtering in the index, but that also seems a weird place to do that.
This approach does work, but I don't want to screw myself later on with it. I'm planning a simple search which should take the filter into account.
In other words, how can I organize the code in such a way that getting data from the database will take the filter into account which has been set? (Session states maybe?)
Route::get('shows/format/{format}',[
'as' => 'shows.format',
'uses' => 'ShowsController#index'
]);
I think you're on the right track here. I would go so far as to produce a factory and inject it into the controller. The purpose of this factory is to construct a formatter that will supply your view with the correct data:
// ShowController
public function __construct(ShowFormatFactory $factory, ShowRepository $shows)
{
$this->factory = $factory;
// NB: using a repository here just for illustrative purposes.
$this->shows = $shows;
}
public function index($format = null)
{
$formatter = $this->factory->make($format);
return View::make('shows.index', [
'formatter' => $formatter,
'shows' => $this->shows->all(),
]);
}
// ShowFormatFactory
class ShowFormatFactory
{
public function make($format)
{
switch($format) {
case 'blueray':
return new BluerayFormat(); break;
case 'dvd': /* Fallthrough for default option */
default:
return new BluerayFormat(); break;
}
}
}
// ShowFormatInterface
interface ShowFormatInterface
{
public function format(Show $show);
}
// BluerayFormat
class BluerayFormat implements ShowFormatInterface
{
public function format(Show $show)
{
return $show->blueray_format;
}
}
Then in your view, since you are guaranteed to have an object that will provide you the format requested for a given show, just call it:
#foreach($shows as $show)
<div class="show">
Chosen Format: {{ $formatter->format($show) }}
</div>
#endforeach
This solution is testable and extensible will allow you to add other formats later on. If you do, you would need to add a discrete case statement in the factory for each different format, as well as write a rather slim ~5-7 line class to support the new format.
So I'm making an API that produces a json response instead of doing View::make('foo', compact('bar')).
With blade templating
My controller simply returns the Eloquent model for Users:
public function index()
{
return User::all();
}
protected function getFooAttribute()
{
return 'bar';
}
And my view will be able to use it, along with the foo attribute (which isn't a field in the user's table).
#foreach($users as $user)
<p>{{$user->name}} {{$user->foo}}</p>
#endforeach
With Angular JS + json response
However, now that I'm not using blade but rather grabbing the json and displaying it with Angular JS I'm not able to do this:
<p ng-repeat="user in users">{{user.name}} {{user.foo}}</p>
Is there a way to cleanly pack the json response such that I have:
[{"name": "John", "foo": "bar"}, ...]
Warning: I've never built an API before and I've only started programming/web dev last December. This is my attempt:
public function index()
{
$response = User::all();
foreach($response as $r)
{
$r->foo = $r->foo;
}
return $response;
}
Yeah there is, example:
return Response::json([User:all()], 200);
Usually you want more than that though..
return Response::json([
'error' => false,
'data' => User:all()
], 200);
200 is the HTTP Status Code.
To include the attributes you need to specify these attributes to automatically append onto the response in your model.
protected $appends = array('foo');
public function getFooAttribute()
{
return 'bar';
}