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.
Related
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);
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.
This is the Laravel ProductController and Products has a many-to-many relationship with Tags.
/**
* Display a listing of the resource.
*
* #return \Illuminate\Http\Response
*/
public function index()
{
$products = Product::with('tags')->latest()->get();
return response()->json($products);
}
On the json response, if I map the products, product.tag is returning an array of objects.
[{"name": "shirt"}, {"name": "red"}]
Is there a way to get only the name property at the with('tags') at the controller, like:
["shirt", "red"]
Also I have been trying something like this:
/**
* Display a listing of the resource.
*
* #return \Illuminate\Http\Response
*/
public function index()
{
$products = Product::with(['tags' => function ($query) {
$result = $query->select(['name']);
return $result->get();
}])->latest()->get();
return response()->json($products);
}
Its possible to filter the data inside tags function?
You should probably use a Resource class to return just the items required in your API. This includes being able to process child relationships. See https://laravel.com/docs/6.x/eloquent-resources#generating-resources
Or, you can do it the way you tried with select but more like;
$products = Product::with(['tags' => function ($query) {
return $query->select(['name']);
}])->latest()->get();
I would like to add query params to my rest api. I created that in Resource Collections and now I cant find a way to add this functionality. Every tutorial is for other ways of creating api. I would like to add to endpoint /api/v1/product filtering by product code something like this:
/api/v1/product?product_code=0208588343711. This is my code
Product Controller:
public function index(): ProductCollection
{
return new ProductCollection(Product::paginate());
}
ProductCollection
class ProductCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
return parent::toArray($request);
}
}
If someone need a answer so I did this in this way:
public function index(): ProductCollection
{
if (request()->input('product_code')){
return new ProductCollection(Product::where('product_code', 'LIKE', request()->input('product_code'))->get());
}
return new ProductCollection(Product::paginate());
}
I have a simple trait which I use to always include soft-deleted items for a few things:
trait OverrideTrashedTrait {
public static function find($id, $columns = ['*'])
{
return parent::withTrashed()->find($id, $columns);
}
}
However, since upgrading to Laravel 5.1, this no longer works. Soft-deleted items do not turn up in get() lists, and if I try to access a page where I've used route model bindings, I get the NotFoundHttpException.
Laravel's upgrade documentation states that:
If you are overriding the find method in your own models and calling parent::find() within your custom method, you should now change it to call the find method on the Eloquent query builder:
So I changed the trait accordingly:
trait OverrideTrashedTrait {
public static function find($id, $columns = ['*'])
{
return static::query()->withTrashed()->find($id, $columns);
}
}
But it appears that no matter what I write in there, it doesn't affect the results. I have also tried to put the overriding find() method directly in the model, but that doesn't appear to be working either. The only way anything changes is if I write invalid syntax. Even if I change the $id to a hardcoded id of an item that is not soft-deleted, I get the same result, and absolutely nothing happens if I e.g. try to dd('sdfg'), so I doubt the method is even called.
Edit: If I do trigger the method manually, it works just like intended.
How can I fix this?
Ok here it goes:
short version: Model binding does not use find.
longer version:
/**
* Register a model binder for a wildcard.
*
* #param string $key
* #param string $class
* #param \Closure|null $callback
* #return void
*
* #throws NotFoundHttpException
*/
public function model($key, $class, Closure $callback = null)
{
$this->bind($key, function ($value) use ($class, $callback) {
if (is_null($value)) {
return;
}
// For model binders, we will attempt to retrieve the models using the first
// method on the model instance. If we cannot retrieve the models we'll
// throw a not found exception otherwise we will return the instance.
$instance = new $class;
if ($model = $instance->where($instance->getRouteKeyName(), $value)->first()) {
return $model;
}
// If a callback was supplied to the method we will call that to determine
// what we should do when the model is not found. This just gives these
// developer a little greater flexibility to decide what will happen.
if ($callback instanceof Closure) {
return call_user_func($callback, $value);
}
throw new NotFoundHttpException;
});
}
Line 931 of Illuminate\Routing\Router says it does:
$instance->where($instance->getRouteKeyName(), $value)->first()
It uses the key from the model used in a where and loads the first result.
In Laravel 5.1 find() method can be found in Illuminate\Database\Eloquent\Builder
From your Model class you can override it like following:
namespace App;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
/**
* Overrides find() method in Illuminate\Database\Eloquent\Builder.
* Finds only active products.
*
* #param mixed $id
* #param array $columns
* #return \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection|null
*/
public static function find($id, $columns = ['*']) {
// create builder from the Model
$builder = (new self)->newQuery();
// method customization
if (is_array($id)) {
// findMany() also should be customized
return self::findMany($id, $columns);
}
$builder->getQuery()->where("isActive", '=', 1)->where($builder->getModel()->getQualifiedKeyName(), '=', $id);
return $builder->first($columns);
}
}