how to add more information to a laravel api resource? - php

I was required to return this json, I am using api resources in Laravel:
{
"data": [
{
"type": "users",
"id": "1",
"attributes": {
"name": "test name",
"lastname": "test lastname"
"projects": 2
},
"relationships": {
"projects": {
"data": [
{
"id": 1,
"type": "projects"
}
]
}
}
}
],
"included": [
{
"type": "projects",
"id": 1,
"attributes": {
"title" : "Test",
"description": "Test",
....
....
}
}
]
}
A user has many projects, I am doing this:
ProjectCollection.php
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
class ProjectCollection extends ResourceCollection
{
public function toArray($request)
{
return [
'data' => $this->collection
];
}
}
ProjectResource.php
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class ProjectResource extends JsonResource
{
public function toArray($request)
{
return [
"type" => "projects",
"id" => $this->id,
"attributes" => [
"title" => $this->title,
"description" => $this->description,
....
....
]
];
}
}
UserCollection.php
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
class UserCollection extends ResourceCollection
{
public function toArray($request)
{
return [
'data' => $this->collection
];
}
public function with($request)
{
return [
//'included' => ProjectResource::collection($this->collection->map->only(['firstProject']) it doesn't work
'included' => new ProjectCollection($this->collection->map->only(['firstProject']) // it doesn't work
];
}
}
UserResource.php
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
public function toArray($request)
{
return [
"type" => "users",
"id" => $this->id,
"attributes" => [
"name" => $this->name,
"lastname" => $this->lastname
"projects" => $this->whenCounted('projects')
],
"relationships" => [
"projects" => new ProjectCollection($this->firstProject),
]
];
}
}
Models/User.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
class User extends Model
{
use HasFactory;
protected $guarded = ['id'];
public function projects()
{
return $this->hasMany(Project::class);
}
public function firstProject()
{
return $this->projects()->oldest()->limit(1);
}
}
UsersController.php
$users = User::withCount('projects')->latest()->get();
return new UserCollection($users);
I am getting this error:
ErrorException PHP 8.1.1
9.39.0 Attempt to read property "id" on array
What can I do? Thank you.

I was able to solve this issue this way:
UserCollection.php
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
class UserCollection extends ResourceCollection
{
public function toArray($request)
{
return [
'data' => $this->collection
];
}
public function with($request)
{
$collection1 = $this->collection->map->only(['firstProject']);
$collection2 = $collection1->map(function($item, $key) {
return $item['firstProject'];
});
return [
'included' => $collection2
];
}
}
Now I have another issue missing, but I will post another question, thanks

Related

Laravel Resource API customize outer data wrapper $wrap

I am trying to rename my data wrapper for the resource I am fetching using Laravel resource. I read in the documentation here how you are supposed to do it, so I did:
ScanResource.php
class ScanResource extends JsonResource
{
public static $wrap = 'scan';
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
//return parent::toArray($request);
return ['id' => $this->id,
'rftag' => $this->rftag,
'jc_number' => $this->jc_number,
'station' => $this->station,
'agent' => $this->agent,
'created_at' => $this->created_at->format('d/m/Y H:i:s'),
'updated_at' => $this->updated_at->format('d/m/Y'),];
}
}
AppServiceProvider.php
class AppServiceProvider extends ServiceProvider
{
public function boot()
{
//Paginator::useBootstrapThree();
Schema::defaultStringLength(191);
//JsonResource::withoutWrapping();
//Resource::withoutWrapping();
ScanResource::withoutWrapping();
}
public function register()
{
//
}
}
This is how I am trying to fetch the resource in my controller:
public function show($id)
{
$product = ScanDetail::find($id);
if (is_null($product)) {
return $this->sendError('Scan details not found.');
}
return $this->sendResponse(new ScanResource($product), 'Scan info retrieved successfully.');
}
currently I am getting the following JSON with Postman:
{
"success": true,
"data": {
"id": 1,
"rftag": "E200203204205212165166",
"jc_number": "15",
"station": "Repairing",
"agent": "kbailey",
"created_at": "11/06/2020 01:29:53",
"updated_at": "11/06/2020"
},
"message": "Scan info retrieved successfully."
}
But I want:
{
"success": true,
"scan": {
"id": 1,
"rftag": "E200203204205212165166",
"jc_number": "15",
"station": "Repairing",
"agent": "kbailey",
"created_at": "11/06/2020 01:29:53",
"updated_at": "11/06/2020"
},
"message": "Scan info retrieved successfully."
}
I tried this, this, this, this and this. This is not what I am using, so I did not think it would work. I also tried modifying my toArray to:
return [
'scan'=>['id' => $this->id,
'rftag' => $this->rftag,
'jc_number' => $this->jc_number,
'station' => $this->station,
'agent' => $this->agent,
'created_at' => $this->created_at->format('d/m/Y H:i:s'),
'updated_at' => $this->updated_at->format('d/m/Y'),]
];
but its giving the following JSON:
{
"success": true,
"data": {
"scan": {
"id": 1,
"rftag": "E200203204205212165166",
"jc_number": "15",
"station": "Repairing",
"agent": "kbailey",
"created_at": "11/06/2020 01:29:53",
"updated_at": "11/06/2020"
}
},
"message": "Scan info retrieved successfully."
}
Again, not what I want since I will be fetching different resources from the database using api calls. So I want to customize the outer wrapper. Any assistance is/will be greatly appreciated. Thanks in advance.
You don't need to do anything in your AppServiceProvider.php
You just need to create ScanResource.php as below:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class ScanResource extends JsonResource
{
/**
* The "data" wrapper that should be applied.
*
* #var string
*/
public static $wrap = 'scan';
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request $request
* #return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
*/
public function toArray($request)
{
return [
'id' => $this->id,
'rftag' => $this->rftag,
'jc_number' => $this->jc_number,
'station' => $this->station,
'agent' => $this->agent,
'created_at' => $this->created_at->format('d/m/Y H:i:s'),
'updated_at' => $this->updated_at->format('d/m/Y')
];
}
public function with($request)
{
return [
"success" => true,
"message": "Scan info retrieved successfully."
];
}
}
And you need to use that ScanResource.php in the ScanController.php as below
// use the namespace at the top
use App\Http\Resources\UserResource;
// add the below action in the controller.
public function show($id)
{
$scan= Scan::find($id);
return new ScanResource($scan);
}
I have tested the code in my postman and I got below output
{
"scan": {
"id" : 7,
"rftag" : "abcde",
"jc_number" :"AB123FG" ,
"station" : "abc",
"agent" : "Stanton Satterfield",
"created_at": "29/10/2021 05:18:42",
"updated_at": "29/10/2021 05:18:42"
},
"success" : true,
"message": "Scan info retrieved successfully."
}
in your controller change to this
return ['scan' => YourResource::collection(YourModel::get())];
and in your AppServiceProvider.php file add this
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Http\Resources\Json\JsonResource; // this
class AppServiceProvider extends ServiceProvider
{
public function register()
{
JsonResource::withoutWrapping(); // and this
}
}
hope it's work for you :)

Conditionaly Laravel Resource

TicketResource.php
public function toArray($request) {
return [
'id' => $this->id,
'user_id' => $this->user_id,
'title' => $this->title,
'body' => $this->body,
'status' => $this->status,
'created_at' => $this->created_at->toDateTimeString(),
];
}
CommentResource.php
public function toArray($request) {
return [
'id' => $this->id,
'body' => $this->body,
'user_id' => $this->user_id,
'created_at' => $this->created_at->toDateTimeString()
];
}
TicketController.php
public function index() {
return TicketResource::collection(Ticket::all());
}
public function show(Ticket $id) {
$ticket = $id;
return new TicketResource($ticket);
}
Model Ticket.php
public function comments() {
return $this->hasMany('App\Comment');
}
Model Comment.php
public function ticket() {
return $this->belongsTo('App\Ticket');
}
routes/api.php
Route::get('tickets', 'TicketController#index');
Route::get('tickets/{id}', 'TicketController#show');
I want when I request to tickets/{id} URL, I expect to receive this response:
{
"data": {
"id": 1,
"user_id": 2,
"title": "lorem",
"body": "epsum",
"status": "open",
"created_at": "2020-03-04 18:14:56",
"comments": [
{
"id": 1,
"body": "equi",
"user_id": 1,
"created_at": "2020-03-05 18:14:56",
}
]
}
}
On the contrary, when I visit tickets URL, I don't want the comments to be added on each ticket.
How can I implement that?
You need to add relation
This is my model class:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Category extends Model
{
public function children()
{
return $this->hasMany(Category::class, 'parent_id', 'id')->select('categories.id',
'categories.cid AS key',
'categories.name AS label',
'categories.type',
'categories.lvl');
}
}
in my controller:
$parents = Category::select(
'categories.id',
'categories.id AS key',
'categories.name AS label')->where('lvl', 1)->get();
foreach ($parents as $item) {
$item->children
}
return Response::json($parents, 200, array('Content-Type' => 'application/json;charset=utf8'), JSON_UNESCAPED_UNICODE);
Result:
[
{
"id":2,
"key":2,
"label":"parent label",
"children":[
{
"id":17,
"key":"92697f63-5c50-11ea-80df-5cf9ddf839d3",
"label":"child label",
"type":"Category",
"lvl":2,
}
]
}
]

How to validate array in Laravel with Request?

I send to Laravel this JSON data:
[
{"name":"...", "description": "..."},
{"name":"...", "description": "..."}
]
I have a StoreRequest class extends FormRequest:
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreRequest extends FormRequest
{
public function authorize()
{
return true;
}
public function rules()
{
return [
'name' => 'required|string|min:1|max:255',
'description' => 'nullable|string|max:65535'
];
}
}
In my controller I have this code, but it doesn't work with array:
public function import(StoreRequest $request) {
$item = MyModel::create($request);
return Response::HTTP_OK;
}
I found this solution to handle arrays in the Request rules():
public function rules()
{
return [
'name' => 'required|string|min:1|max:255',
'name.*' => 'required|string|min:1|max:255',
'description' => 'nullable|string|max:65535'
'description.*' => 'nullable|string|max:65535'
];
}
How can I update the StoreRequest and/or the import() code to avoide duplicate lines in rules()?
As you have an array of data you need to put * first:
public function rules()
{
return [
'*.name' => 'required|string|min:1|max:255',
'*.description' => 'nullable|string|max:65535',
];
}

ci_phpunit_test: Help needed about test double with queries

i'm learning about unit testing with CodeIgniter and i would to ask some questions about testing queries with Mocks class.
I tried to implement the following class News_model with the method get_news_all() that returns all news data from table 'news' and get_news returns only title and text fields from the same.
class News_model extends CI_Model
{
public function __construct(){
$this->load->database();
}
public function get_news_all()
{
$query=$this->db->get('news');
$result=$query->result_array();
return $result;
}
public function get_news()
{
$this->db->select('title, text');
$this->db->from('news');
$query=$this->db->get();
$result=$query->result_array();
return $result;
}
After i tried to build a News_model_with_mocks_test for testing method get_news_all() and in this case test runs fine:
class News_model_with_mocks_test extends TestCase
{
public function setUp()
{
$this->resetInstance();
$loader=$this->getMockBuilder('CI_Loader')->setMethods(['database'])->getMock();
$loader->method('database')->willReturn($loader);
$this->CI->load=$loader;
if(!class_exists('CI_DB', false))
{
eval('class CI_DB extends CI_DB_query_builder {}');
}
$this->obj=new News_model();
}
public function test_1()
{
$result_array = [
[
"id" => "1",
"title" => "News",
"slug" => "news",
"text" => "News",
],
[
"id" => "2",
"title" => "News2",
"slug" => "news2",
"text" => "News2",
],
];
$db_result=$this->getMockBuilder('CI_DB_result')->disableOriginalConstructor()->getMock();
$db_result->method('result_array')->willReturn($result_array);
$db = $this->getMockBuilder('CI_DB')->disableOriginalConstructor()->getMock();
$db->expects($this->once())->method('get')->with('news')->willReturn($db_result);
$this->obj->db=$db;
$result=$this->obj->get_news_all();
$this->assertEquals($result_array,$result);
}
}
But i don't know how to do tests for the method get_news(), i tried something as this:
public function test_1()
{
$result_array2 = [
[
"title" => "News",
"text" => "News",
],
[
"title" => "News2",
"text" => "News2",
],
];
$db_result=$this->getMockBuilder('CI_DB_result')->disableOriginalConstructor()->getMock();
$db_result->method('result_array')->willReturn($result_array2);
$db = $this->getMockBuilder('CI_DB')->disableOriginalConstructor()->getMock();
$db->expects($this->once())->method('query')->with('select title,text from news')->willReturn($db_result);
$this->obj->db=$db;
$result=$this->obj->get_news();
$this->assertEquals($result_array2,$result);
}
phpunit thows the following exception:
PHP Fatal error: Call to a member function result_array() on a non- object in /opt/lampp/htdocs/codeigniter/application/models/Users_model.php on line 21
I don't know how to test double with select queries! Thank you in advance for your answers.
i read more documentation in these days and i understood that it was a misunderstanding for me about usage of Mocks. In other words we have to define methods and returning values that we will expect and inject them to original classes. This is the method get_users of the class News_model that i wrote above:
public function get_news()
{
$this->db->select('title, text');
$this->db->from('news');
$query=$this->db->get();
$result=$query->result_array();
return $result;
}
We simply want that the get() method will return a result array which contains only title and text fields for each record:
class News_model_with_mocks_test extends TestCase
{
public function setUp()
{
$this->resetInstance();
$loader=$this->getMockBuilder('CI_Loader')->setMethods(['database'])->getMock();
$loader->method('database')->willReturn($loader);
$this->CI->load=$loader;
if(!class_exists('CI_DB', false))
{
eval('class CI_DB extends CI_DB_query_builder {}');
}
$this->obj=new News_model();
}
public function test_1()
{
$result_array = [
[
"title" => "News test",
"text" => "News text",
],
[
"title" => "News2",
"text" => "Testo news2",
],
];
$db_result=$this->getMockBuilder('CI_DB_result')->disableOriginalConstructor()->getMock();
$db_result->method('result_array')->willReturn($result_array);
$db = $this->getMockBuilder('CI_DB')->disableOriginalConstructor()->getMock();
$db->expects($this->once())->method('get')->willReturn($db_result);
$this->obj->db=$db;
$result=$this->obj->get_news();
$this->assertEquals($result_array,$result);
}
}
I hope that this solution could help someone that could have same doubts!

Laravel Dingo API - How to respond with multiple collections / transformers?

To initialize my app I have the following route:
/initialize
This returns Taxonomies, Enumerables and a couple of other taxonomy like collections. This saves multiple HTTP requests.
Although with Dingo / Fractal, I cannot see how I can respond with multiple collections?
e.g.
return [
'taxonomies' => $this->response->collection($taxonomies, new TaxonomyTransformer);
'enumerables' => $this->response->collection($enumerables, new EnumerableTransformer);
'otherStuff' => $this->response->collection($otherStuff, new OtherStuffTransformer);
];
return response()->json([
'data' => [
'taxonomies' => $this->fractal->collection($taxonomies, new TaxonomyTransformer);
'enumerables' => $this->fractal->collection($enumerables, new EnumerableTransformer);
'otherStuff' => $this->fractal->collection($otherStuff, new OtherStuffTransformer);
]
], 200);
This should return the JSON in the format you are looking for.
I have the same issue ,and I found the solution from How to use Transformer in one to many relationship. #1054.
Here is the collection I want to return with the transfomer of dingo in my controller.
$user = User::where('email','=',$input['email'])->with('departments')->with('roles')->get();
DepartmentTransformer
class DepartmentTransformer extends TransformerAbstract
{
public function transform($department)
{
return [
'id' => $department['id'],
'name' => $department['name'],
'level' => $department['level'],
'parent_id' => $department['parent_id']
];
}
}
RolesTransformer
class RolesTransformer extends TransformerAbstract
{
public function transform($role)
{
return [
'name' => $role['name'],
'slug' => $role['slug'],
'description' => $role['description'],
'level' => $role['level']
];
}
}
UserTransformer
class UserTransformer extends TransformerAbstract
{
protected $defaultIncludes = ['departments','roles'];
public function transform($user)
{
return [
'id' => $user['id'],
'name' => $user['name'],
'email' => $user['email'],
'phone' => $user['phone'],
];
}
public function includeDepartments(User $user)
{
$dept = $user->departments;
return $this->collection($dept, new DepartmentTransformer());
}
public function includeRoles(User $user)
{
$rl = $user->roles;
return $this->collection($rl, new RolesTransformer());
}
}
In my controller
$user = User::where('email','=',$input['email'])->with('departments')->with('roles')->get();
return $this->response->collection($user, new UserTransformer());
And I got the result
"data": {
{
"id": 43,
"name": "test7",
"email": "test7#foxmail.com",
"phone": "186********",
"departments": {
"data": {
{
"id": 1,
"name": "业务一部",
"level": 1,
"parent_id": 0
}
}
},
"roles": {
"data": {
{
"name": "agent",
"slug": "agent",
"description": "业务员",
"level": 1
}
}
}
}
}
Please take note of the usage of $defaultIncludes and includeXXX() methonds in the UserTransformer.You can get more detail info from Fractal Doc.

Categories