How to customize Laravel's Collection API resources? - laravel

I tried the following but it doesn't work:
ArticleController.php:
public function index()
{
$articles = Article::latest()->paginate(10);
return ArticleCollection::collection($articles);
}
ArticleCollection.php:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
class ArticleCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* #param \Illuminate\Http\Request $request
* #return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
*/
public function toArray($request)
{
return [
'name' => $this->title,
'subtitle' => $this->subtitle,
'cover' => $this->cover,
];
}
}
This of transforming the model seems to work for a single model, but not for a collection. How should i customize which fields will be returned when working with collections API resources?

Let your DB query define the final fields
Check this way, in your ArticleController create a query with a select() method in which you define which DB fields are sent to the ArticleCollection.
The collection will only take care of returning the that that was given to it!
public function index()
{
return new ArticleCollection(
Article::latest()->select(['name', 'subtitle', 'cover'])->paginate(10)
);
}
Result query
select
`name`,
`subtitle`,
`cover`
from
`articles`
order by
`created_at` desc
limit
10 offset 0
ArticleCollection
This file can be set as default, this allows you to dynamically set values passed from the select() method on the ArticleController.
class ArticleCollection extends ResourceCollection
{
public function toArray($request)
{
return parent::toArray($request);
}
}

You can use it like this:
return new ArticleCollection($articales);

Related

Practical way to only return selected columns in Laravel in method

I am doubting (life of a junior developer) what would be the most practical way to only return the value from a specific column in Laravel of a database record.
Example controller method:
public function show(ProductsCategory $category)
{
return $category;
}
This outputs all the columns, like this
{"id":104,"category_name":"Soft drinks","created_at":"2021-06-09T17:16:54.000000Z","updated_at":"2021-06-09T17:16:54.000000Z"}
However what I am after is just getting the category_name column retuned, like this
{"category_name":"Soft drinks"}
I can accomplish this by doing
public function show($id)
{
$category = ProductsCategory::select('category_name')->findOrFail($id);
echo json_encode($category)
exit;
}
However doubting if this would be the most practical way to go? Is there an more elegant/straight forward way? Or am I grossly overthinking this?
I think its developer choice . One way to select column like you mentioned .Another way is like below
$category = ProductsCategory::findOrFail($id,['category_name']);
By default findOrFail($id, $columns = ['*']) return all columns so they mentioned *
Also instead of json_encode as json,you can directly return $category
Also if you want to pass custom headers or status code then you can return like below
return response()->json($category)
Here is json method params
/**
* Create a new JSON response instance.
*
* #param mixed $data
* #param int $status
* #param array $headers
* #param int $options
* #return \Illuminate\Http\JsonResponse
*/
public function json($data = [], $status = 200, array $headers = [], $options = 0);
If you want to get full control over returned resource you should use API Resources.
To create resource file run:
php artisan make:resource ProductsCategoryResource
In resource file you can define fields to return, i.e. if you need only id and name you do this:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class ProductCategoryResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
];
}
}
Then, set response in controller:
public function show($id)
{
$category = ProductsCategory::select('id', 'name')->findOrFail($id);
return new ProductCategoryResource($category);
}
You can also return resource collection:
public function index()
{
$categories = ProductsCategory::select('id', 'name')->get()l
return ProductCategoryResource::collection($category);
}
I think this is really elegant and organized way.

How can I modify the Laravel API Resource collection response to include another 'data' wrapper with in the 'data' wrapper?

<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
class LanguageCollection extends ResourceCollection
{
public $collects = LanguageResource::class;
/**
* TagCollection constructor.
* #param $resource
*/
public function __construct($resource)
{
parent::__construct($resource);
}
/**
* Transform the resource collection into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
return [
'languages' => parent::toArray($request)
];
}
}
This would give a response structure as follows:
But I want to put all the languages with in another wrapper called "result" so the response structure will look like this:
I don't want to hard code it this way cause this would require us to change every resource collecction.
public function toArray($request)
{
return [
'result' => ['languages' => parent::toArray($request)]
];
}
What is the correct way to achieve this response structure?

How get Date format in notifications collection

I need to define the date format for a collection. I tried it that way, but it doesn't work:
return response()->json($request->user()->notifications()->format('d/m/Y')->limit(7)->get());
How could I set the date format for an entire collection?
I reckon you are using database notifications and you want to format the created_at field.
To return the results fast, you might do something like this:
$notifications = $user->notifications()
->limit(7)
->get()
->each(function ($notification) {
$notification->formatted_created_at = $notification->created_at->format('d/m/Y');
});
I suggest that you do it properly and create a new API resource.
use Illuminate\Http\Resources\Json\JsonResource;
class NotificationResource extends JsonResource
{
/**
* Transform the notification into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
return [
'id' => $this->id,
// ...
'formatted_created_at' => $this->created_at->format('d/m/Y'),
];
}
}
// In the controller action
$notifications = $user->notifications()
->limit(7)
->get();
return NotificationResource::collection($notifications);

How to set a maximum per page in laravel dynamically?

I have a Products class that looks for these paginated items, but in the front end I allow the user to define how many items he wants to display per page (10, 30, 50, 100) the problem is that if someone passes 1000, the api returns 1000 records per page.
How can I validate this for all controllers and models dynamically?
I could do this "easily" by validating each request ('limit') on each controller, but it would not be practical, how can I do that?
public function index(Request $request)
{
$perPage = $request->input('limit'); // User input
$sort = 'global_performance';
$descending = 'desc';
$products = Product::where('status', 1)
->orderBy($sort, $descending)
->paginate($perPage); //
return $products;
}
You can validate the limit like this:
public function index(Request $request)
{
$this->validate($request, [
'limit' => ['required', 'integer', Rule::in([10, 30, 50, 100])]
]);
$perPage = $request->input('limit'); // User input
$sort = 'global_performance';
$descending = 'desc';
$products = Product::where('status', 1)
->orderBy($sort, $descending)
->paginate($perPage); //
return $products;
}
Now, add following line in just before controller class:
use Illuminate\Validation\Rule;
Update
More dynamic way might be creating custom request class like this:
Run following command to create a new form request class:
php artisan make:request PaginateRequest
This will create PaginateRequest class at App\Http\Requests directory like this:
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class PaginateRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
return false;
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
//
];
}
}
Now change this class into following:
<?php
namespace App\Http\Requests;
use Illuminate\Validation\Rule;
use Illuminate\Foundation\Http\FormRequest;
class PaginateRequest extends FormRequest
{
/**
* 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 [
'limit' => ['required', 'integer', Rule::in([10, 30, 50, 100])]
];
}
}
After this, you can use in controller function by adding it as function parameter.
public function index(PaginateRequest $request)
{
$perPage = $request->input('limit'); // User input
$sort = 'global_performance';
$descending = 'desc';
$products = Product::where('status', 1)
->orderBy($sort, $descending)
->paginate($perPage); //
return $products;
}
Please don't forget to import it just before controller class like this:
use App\Http\Requests\PaginateRequest;
In this way, You can use this request class everywhere you need.
You can see more at documentation here: https://laravel.com/docs/5.8/validation
You could easily create a middleware. Apply it simply from kernel to each and every route or make a group in your route file to apply it to selective routes.
Inside the middleware just check the limit, if empty or more than the max limit you want let's say 100, make it 100 like so:
$limit = $request->input('limit');
if (empty($limit) || ($limit > 100)) {
$request['limit'] = 100;
}
wouldn't that work?
Here is link for middlewares in Laravel.

Laravel API resourceCollection using array rather than model

I have an API that uses API resource and resource collections to correctly format the JSON responses. In order to decouple my controller from my model I use an adapter to query the underlying model. I'd like to pass the adapter return values as arrays, rather than Eloquent models, to ensure that any furture adapters are easier to right in respect to their return data structures. To create the array return values I serialise my adapter Eloquent results with ->toArray().
I have 2 API Resources to correctly format these results, for a single resource I have:
use Illuminate\Http\Resources\Json\Resource;
class Todo extends Resource
{
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
return $this->resource;
}
}
For a resource collection I have:
use Illuminate\Http\Resources\Json\ResourceCollection;
class TodoCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
return [
'data' => $this->collection
->map
->toArray($request)
->all()
];
}
}
When I return a single resource from my controller with :
use App\Http\Resources\Todo;
public function show($id)
{
return new Todo($this->todoAdapter->findById($id));
}
and the adapter query as:
public function findById(int $id){
return TodoModel::findOrFail($id)
->toArray();
}
This works as expected. The problem comes when I try to pass an array of a collection of models i.e.
public function index(Request $request)
{
$todos = $this->todoAdapter->getAllForUserId(Auth::id(), 'created_by', 'desc', self::DEFAULT_PAGINATE);
return new TodoCollection($todos);
}
and the adapter query as:
public function getAllForUserId(int $userId, string $sortField, string $sortDir, int $pageSize = self::DEFAULT_PAGINATE)
{
return Todo::BelongsUser($userId)
->orderBy($sortField, $sortDir)
->paginate($pageSize)
->toArray();
}
I get the following error:
"message": "Call to a member function first() on array",
"exception": "Symfony\\Component\\Debug\\Exception\\FatalThrowableError",
"file": "/home/vagrant/code/public/umotif/vendor/laravel/framework/src/Illuminate/Http/Resources/CollectsResources.php",
"line": 24,
I'm guessing that I can't do 'new TodoCollection($todos)' where $todos is an array of results. How would I get my todoCollection to work with arrays? Any suggestions would be much appreciated!
Your collections toArray is trying to do too much:
$this->collection
->map
->toArray($request)
->all()
Just directly call $this->collection->toArray().
Just to update this. In the end I found that creating a collection from the array of results and passing that to the resource collection constructor worked, though I did have to add explicit mappings within the resource collection for links and meta etc.

Resources