Eloquent (kind of) triple relationship - laravel

I'm having a hard time finding a simple solution, using Eloquent relationships, I have 3 models:
User
Item
Category
Now a User has many Categories, a Category only has one user. A user has many items, an item as many users. And finally an item has many categories and many categories has many items. The tricky part is that although an item can have many categories, it can only has one category from each user, so the reverse would be the user can only associate one category that he owns to the item.
Also the pivot table between item and user(item_user) has two attributes, boolean is_owner and tinyInt permissions
During my application I always have the user ID
I would like to retrieve the expenses that belong to the user, achieving something like this:
[
{
"id": "1",
"value": "0.40",
"description": "Expense 0",
"category": {
"id": "1",
"name": "Health",
"created_at": {
"date": "2015-04-15 01:33:05",
"timezone_type": 3,
"timezone": "UTC"
},
"update_at": {
"date": "2015-04-15 01:33:05",
"timezone_type": 3,
"timezone": "UTC"
}
},
"users": [
{
"id": "1",
"name": "Fabio",
"email": "fabioantuness#gmail.com",
"permissions": {
"expense_id": "1",
"user_id": "1",
"is_owner": "1",
"permissions": "6"
}
}
]
},
{
"id": "2",
"value": "12.40",
"description": "Expense 1",
"category": {
"id": "1",
"name": "Health",
"created_at": {
"date": "2015-04-15 01:33:05",
"timezone_type": 3,
"timezone": "UTC"
},
"update_at": {
"date": "2015-04-15 01:33:05",
"timezone_type": 3,
"timezone": "UTC"
}
},
"users": [
{
"id": "1",
"name": "Fabio",
"email": "fabioantuness#gmail.com",
"permissions": {
"expense_id": "2",
"user_id": "1",
"is_owner": "1",
"permissions": "6"
}
}
]
}
]
Item model:
class Item extends Model {
protected $fillable = ['category_id', 'value', 'description'];
public function users()
{
return $this->belongsToMany('App\User')->withPivot('is_owner', 'permissions');
}
public function categories()
{
return $this->belongsToMany('App\Category');
}
}
User model:
class User extends Model implements AuthenticatableContract, CanResetPasswordContract {
use Authenticatable, CanResetPassword;
protected $table = 'users';
protected $fillable = ['name', 'email', 'password'];
protected $hidden = ['password', 'remember_token'];
public function categories()
{
return $this->hasMany('App\Category');
}
public function items()
{
return $this->belongsToMany('App\Item')->withPivot('is_owner', 'permissions');
}
public function tokens()
{
return $this->hasMany('App\Token');
}
}
Category model:
class Category extends Model {
protected $fillable = ['name'];
public function user(){
return $this->belongsTo('App\User');
}
public function items()
{
return $this->belongsToMany('App\Item');
}
}

Schematizing the question here on stackoverflow helped me get to the solution, here's the query:
User::expenses()->with(
[
'users',
'categories' => function ($query) use ($userId)
{
$query->where('user_id', $userId);
}
]
)->get()->all();
If anyone has a better solution feel free to share it

Related

Not getting expected data for Laravel JSON:API

I am trying to create an API based on the following two Models:
Product.php
<?php
namespace App\Models\Commerce;
use ...
class Product extends Model {
use HasFactory;
use SoftDeletes;
public mixed $id;
protected $fillable = ['user_id', 'title', 'default_variation', 'status'];
/**
+ * #return HasMany
+ */
public function productVariations() : HasMany {
return $this->hasMany('App\Models\Commerce\ProductVariation');
}
public function defaultVariation() : HasOne {
return $this->hasOne('App\Models\Commerce\ProductVariation', 'default_variation');
}
}
ProductVariation.php
<?php
namespace App\Models\Commerce;
use ...
class ProductVariation extends Model {
use HasFactory;
protected $fillable = ['user_id', 'product_id', 'sku', 'title', 'price', 'status'];
public function product() : BelongsTo {
return $this->belongsTo(Product::class);
}
}
I have configured the JSON:API according to the documents, as best I can tell:
routes/api.php
<?php
use ...
JsonApiRoute::server('v2')->prefix('v2')->resources(function ($server) {
$server->resource('products', JsonApiController::class)->readOnly();
$server->resource('product-variations', JsonApiController::class)->readOnly();
});
ProductSchema.php
<?php
// For authentication see ProductPolicy
namespace App\JsonApi\V2\Products;
use ...
class ProductSchema extends Schema
{
public static string $model = Product::class;
public function fields(): array
{
return [
ID::make(),
DateTime::make('createdAt')->sortable()->readOnly(),
DateTime::make('updatedAt')->sortable()->readOnly(),
Str::make('title'),
Number::make('user_id'),
Number::make('defaultVariation'),
HasOne::make('defaultVariation', 'default_variation'),
Boolean::make('status'),
HasMany::make('productVariations')
];
}
public function filters(): array
{
return [
WhereIdIn::make($this),
];
}
public function pagination(): ?Paginator
{
return PagePagination::make();
}
}
ProductVariationSchema.php
<?php
namespace App\JsonApi\V2\ProductVariations;
use ...
class ProductVariationSchema extends Schema
{
public static string $model = ProductVariation::class;
public function fields(): array
{
return [
ID::make(),
DateTime::make('createdAt')->sortable()->readOnly(),
DateTime::make('updatedAt')->sortable()->readOnly(),
Number::make('userId'),
Number::make('productId'),
Str::make('sku'),
Str::make('title'),
Number::make('price'),
Boolean::make('status'),
BelongsTo::make('product')
];
}
public function filters(): array
{
return [
WhereIdIn::make($this),
];
}
public function pagination(): ?Paginator
{
return PagePagination::make();
}
}
With some dummy data in the database, I can access the endpoint /api/v2/products and receive the following:
{
"jsonapi": {
"version": "1.0"
},
"data": [
{
"type": "products",
"id": "3",
"attributes": {
"createdAt": null,
"updatedAt": null,
"title": "foo-product",
"user_id": 1,
"status": 1
},
"relationships": {
"defaultVariation": {
"links": {
"related": "domain/api/v2/products/3/default-variation",
"self": "domain/api/v2/products/3/relationships/default-variation"
}
},
"productVariations": {
"links": {
"related": "domain/api/v2/products/3/product-variations",
"self": "domain/api/v2/products/3/relationships/product-variations"
}
}
},
"links": {
"self": "domain/api/v2/products/3"
}
}
]
}
/api/v2/product-variations
{
"jsonapi": {
"version": "1.0"
},
"data": [
{
"type": "product-variations",
"id": "3",
"attributes": {
"createdAt": null,
"updatedAt": null,
"userId": 1,
"productId": 3,
"sku": "foo-bar",
"title": "foo bar ",
"price": "11.11",
"status": 1
},
"relationships": {
"product": {
"links": {
"related": "domain/api/v2/product-variations/3/product",
"self": "domain/api/v2/product-variations/3/relationships/product"
}
}
},
"links": {
"self": "domain/api/v2/product-variations/3"
}
}
]
}
However, if I try api/v2/products/3/product-variations or /api/v2/products/3/relationships/product-variations I get a 404 error:
{
"jsonapi": {
"version": "1.0"
},
"errors": [
{
"status": "404",
"title": "Not Found"
}
]
}
Additionally, if I run a test I also get a 404 but I do see that the variations are correctly related:
I think that somewhere I have misconfigured something, but this is my first time doing this in Laravel so I might be completely missing a configuration also.
You only set a JsonApiRoute (starting with api) and prefix('v2') meaning api/v2 group. In this group you add 2 more product-variations meaning /api/v2/product-variations and products /api/v2/products. So anything else don't exists and laravel returns 404.
You need to add
$server->resource('products/{id}/product-variations', JsonApiController::class)->readOnly();
for api/v2/products/3/product-variations
and
$server->resource('products/{id}/relationships/product-variations', JsonApiController::class)->readOnly();
for /api/v2/products/3/relationships/product-variations
Also you can add (int $id) in your controller methods to read this id you pass.

data limiting global scope with laravel model

i have a model named "Page" and
I want the first of my model's image arm data to be written, I wonder how can I do it?
public function index($lang)
{
$data = Page::where('language', $lang)->orderBy('order', 'ASC')->with('categori')->get();
if ($data) {
return response()->json($data);
}
return response()->json(['message' => 'Record not found.'], 404);
}
{
"id": 3,
"title": "Conporta",
"contents": "<p>Conporta</p>",
"slug": "hakkimizda",
"image": [
"/storage/page/Conporta/40-banner-1659203850.jpg",
"/storage/page/Conporta/40-banner-small-1659203850.jpg",
"/storage/page/Conporta/40-banner-medium-1659203850.jpg"
],
"language": "en",
"is_home": "false",
"status": "on",
"seo": null,
"order": "0",
"category_id": 2,
"created_at": "2022-07-30T17:57:30.000000Z",
"updated_at": "2022-07-30T17:57:30.000000Z",
}
You can hide the image attribute from model and create new attribute that should hold one image.
...
protected $hidden = ['image'];
protected $appends = ['img'];
...
public function getImgAttribute(){
return $this->image[0];
}

Laravel eloquent get related model as column_name => value

I have a Product table related with Image table, one to many relationship.
How do I get Image records not as array? Instead, return only 1 Image record as column_name: value
This code:
$products = $request->user()->products()->orderBy('id', 'desc')->with(['images' => function($query) {
$query->where('featured', 1)->limit(1);
}])->get();
Is returning data like this :
{
"id": 13,
"name": "Shoes",
"price": "3.00",
"stock": 5,
"pivot": {
"user_id": 7,
"product_id": 13
},
"images": [
{
"id": 5,
"file": "5da9d9b493403.png",
"featured" 1,
"pivot": {
"product_id": 13,
"image_id": 5
}
}
]
}
}
How to make it return like this?
{
"id": 13,
"name": "Shoes",
"price": "3.00",
"stock": 5,
"pivot": {
"user_id": 7,
"product_id": 13
},
"images": "5da9d9b493403.png"
}
}
You can append a custom accessor that gets the first image from the relationship
In the Product model
protected $appends = ['images'];
public function getImagesAttribute()
{
return $this->images()->where('featured', 1)->first()->file;
}
public function images()
{
return $this->hasMany('App\Image');
}
Then just return the query without eager loading
$products = $request->user()->products()->orderBy('id', 'desc')->get();
Hope this helps
In the Product model
protected $appends = ['images'];
public function images()
{
return $this->hasMany('App\Image')->select(['name'])->first();
}
select the name only and return first relation

laravel eloquent with in with select return null

I use laravel eloquent with relations, I have a problem about it.
these are my eloquent models;
class Post extends BaseModel
{
public function user()
{
return $this->belongsTo(User::class, 'user_id', 'id');
}
}
class User extends BaseModel
{
public function userInfo()
{
return $this->hasOne(UserInfo::class,'user_id','id');
}
}
class UserInfo extends BaseModel
{
}
when I use the following code, the user_info is null.
$posts = Post::with(['user' => function ($query) {
$query->with(['userInfo' => function ($query) {
$query->select(['nickname','avatar']);
}]);
}])->paginate(10)
->toArray();
when i request it , the result is
{
"id": 10,
"community_id": 1,
"title": "biaoti",
"desc": null,
"content": "abc",
"user_id": 1,
"user": {
"id": 1,
"mobile_phone": "13800000003",
"user_info": null
}
}
But the following code is normall.
$posts = Post::with(['user' => function ($query) {
$query->with(['userInfo' => function ($query) {
// $query->select(['nickname','avatar']);
}]);
}])->paginate(10)
->toArray();
when i request it ,the result is :
{
"id": 10,
"community_id": 1,
"title": "biaoti",
"desc": null,
"content": "abc",
"user_id": 1,
"user": {
"id": 1,
"mobile_phone": "13800000003",
"user_info": {
"id": 1,
"user_id": 1,
"nickname": "devkang",
"avatar": "",
"birthday": "1989-01-30",
"sex": 1,
"identity": 1,
"region_id": 1
}
}
}
can you help me? I don't know how to use it, I want to select some fields from user_info.
This is happening because you exclude user_id foreign key when you're using select method. To fix it add foreign key to the list:
$query->select(['nickname', 'avatar', 'user_id']);

Laravel's Eloquent pivot object

I have defined relation in my model:
public function positions() {
return $this->belongsToMany('Position', 'users_positions')->withPivot('season');
}
Is it possible to display objects with pivot in cleaner way?
For Example:
"positions": [
{
"id": 1,
"name": "A",
"pivot": {
"user_id": 1,
"position_id": 1,
"season": 2014
}
}
],
I would like to get:
"positions": [
{
"id": 1,
"name": "A",
"season": 2014
}
],
Use accessor:
public function getSeasonAttribute()
{
return ($this->pivot) ? $this->pivot->season : null;
}
Then you can access it just like other properties:
$position = $someModel->positions->first();
$position->season;
if you also need it in the toArray/toJson output, then use on you model:
protected $appends = ['season'];

Resources