Handling revisions - setting attributes or mutators for relation columns - php

Pseudo code - say I have the models Author, Document, Revisions, Editor.
Author hasMany Document
Document hasMany Revisions
Document hasMany Editors (which are stored in the revision table)
But the following table structure:
Author Model: id, name, email
Document Model: id, author_id, title
Revisions Model: id, document_id, editor_id, text, saved_at
Editor Model: id, name, email
First question - to store the revision history (including which editor changed the text at which time); is this an ideal structure? I want to be able to do $author->documents->where('title', 'Some title')->editor->name;
To access the Editor from the Document - is it worth setting attributes directly in the Document constructor:
public function __construct(array $attributes = [] ){
$this->setRawAttributes(
array_merge($this->attributes,
$this->revisions()->orderBy('saved_at', 'desc')->first()->attributesToArray()
)
);
}
Or use mutators in the model:
public function getEditorIdAttribute($value){
return $this->revisions()->orderBy('saved_at', 'desc')->first()->editor_id;
}
Or is there a better way of handling revisions that's more Laravel/Eloquent-like?

For anyone who comes down this path - I wasn't able to set attributes in the constructor and have them available in the Model so I resorted to using mutators.
To prevent a new query every time a mutator was called (which adds up if you have a handful of mutators) - I used a simple workaround:
// Document Model
class Document extends Eloquent{
$this->latest = ''
// relations etc here
public function getSomeValueAttribute{
$this->getLatest('some_value');
}
public function getAnotherValueAttribute{
$this->getLatest('another_value');
}
public function getLatest($attr){
if(empty($this->latest)) $this->latest = $this->revisions->last();
return $this->latest->getAttribute($attr);
}
}
I'm sure I can extend the getValueAttribute() mutator to keep things DRY, but the above works for me for now, and mutators are called before relations are setup so it works quite well. I'm also able to see all my revisions via $document->revisions->get() or just the latest values via $document->text.

Related

Retrieve Parent Model Through Pivot Table Laravel

I'm currently struggling with retrieving data towards a parent model. I'll drop my database, classes, and things I've tried before.
I have 4 tables: sales_orders, products, work_orders, and product_sales_order (pivot table between sales_orders and products).
SalesOrder.php
class SalesOrder extends Model
{
public function products()
{
return $this->belongsToMany(Product::class)
->using(ProductSalesOrder::class)
->withPivot(['qty', 'price']);
}
}
ProductSalesOrder.php
class ProductSalesOrder extends Pivot
{
public function work_orders()
{
return $this->hasMany(WorkOrder::class);
}
public function getSubTotalAttribute()
{
return $this->qty* $this->price;
}
}
WorkOrder.php
class WorkOrder extends Model
{
public function product_sales_order()
{
return $this->belongsTo(ProductSalesOrder::class);
}
public function sales_order()
{
return $this->hasManyThrough(
ProductSalesOrder::class,
SalesOrder::class
);
}
}
So, what I want to retrieve sales order data from work order since both tables don't have direct relationship and have to go through pivot table and that is product sales order. I've tried hasOneThrough and hasManyThrough but it cast an error unknown column. I understand that error and not possible to use that eloquent function.
Is it possible to retrieve that sales order data using eloquent function from WorkOrder.php ?
You cannot achieve what you want using hasOneThrough as it goes from a table that has no ID related to the intermediate model.
In your example you are doing "the inverse" of hasOneThrough, as you are going from a model that has the ID of the intermediate model in itself, and the intermediate model has the ID of your final model. The documentation shows clearly that hasOneThrough is used exactly for the inverse.
So you still should be able to fix this, and use a normal relation as you have the sales_orders_id in your model SuratPerintahKerja, so you can use a normal relation like belongsTo to get just one SalesOrder and define it like this:
public function salesOrder()
{
return $this->belongsTo(SalesOrder::class, 'sale_orders_id');
}
If you want to get many SalesOrders (if that makes sense for your logic), then you should just run a simple query like:
public function salesOrders()
{
return $this->query()
->where('sale_orders_id', $this->sale_orders_id)
->get();
}
Have in mind that:
I have renamed your method from sales_order to salesOrder (follow camel case as that is the Laravel standard...).
I have renamed your method from sales_order to salesOrders for the second code as it will return more than 1, hence a collection, but the first one just works with one model at a time.
I see you use sale_orders_id, but it should be sales_order_id, have that in mind, because any relation will try to use sales_order_id instead of sale_orders_id, again, stick to the standards... (this is why the first code needs more parameters instead of just the model).
All pivot tables would still need to have id as primary and auto incremental, instead of having the id of each related model as primary... Because in SuratPerintahKerja you want to reference the pivot table ProdukSalesOrder but it has to use both produks_id (should have been produk_id singular) and sale_orders_id (should have been sales_order_id). So if you were able to use something like produk_sales_order_id, you could be able to have better references for relations.
You can see that I am using $this->query(), I am just doing this to only return a new query and not use anything it has as filters on itself. I you still want to use current filters (like where and stuff), remove ->query() and directly use the first where. If you also want to add ->where('produks_id', $this->produks_id) that is valid and doesn't matter the order. But if you do so, I am not sure if you would get just one result, so ->get() makes no sense, it should be ->first() and also the method's name should be salesOrder.
Sorry for this 6 tip/step, but super personal recommendation, always write code in English and do not write both languages at the same time like produks and sales orders, stick to one language, preferrably English as everyone will understand it out of the box. I had to translate some things so I can understand what is the purpose of each table.
If you have any questions or some of my code does not work, please tell me in the comments of this answer so I can help you work it out.
Edit:
After you have followed my steps and changed everything to English and modified the database, this is my new code:
First, edit ProductSalesOrder and add this method:
public function sales_order()
{
return $this->belongsTo(SalesOrder::class);
}
This will allow us to use relations of relations.
Then, have WorkOrder as my code:
public function sales_order()
{
return $this->query()->with('product_sales_order.sales_order')->first();
}
first should get you a ProductSalesOrder, but then you can access ->sales_order and that will be a model.
Remember that if any of this does not work, change all the names to camelCase instead of kebab_case.

Cannot get attribute in laravel accessor (October CMS)

I'm currently working on a project with October CMS, and trying to use taglist with relation data.
And I'm using custom accessor to get full name from two columns, seems like a very common case.
But I just couldn't get the model's attributes in the accessor.
Here's my code...
class NameOfClass extends Model
{
/** ALL THE CODE GENERATED BY OCTOBER CMS PLUGIN BUILDER */
public function getFullNameAttribute()
{
return $this->firstname." ".$this->lastname;
}
}
And call the accessor in field.yml file...
fieldName:
label: FieldName
descriptionFrom: description
type: taglist
mode: relation
nameFrom: full_name
customTags: false
I can see the accessor gets called just fine, since I can get the value by changing the returned value to plain string.
I've been spent a lot of time researching for solution... any idea?
I'm developing with the official octobercms docker image latest version.
Its not possible due to internal code. so I suggest do not try to solve it. its not solvable :) try alternative
WHY ??
Because from code taglist is designed in a such way that it will work with TAGS.
it will allow to create new tags if there are no selected tag and if tags are existing then it will attache to given record. and this all will work with real attributes.
so its not design to work with virtual attributes.
For more details this is the code how it generates tags
public function getFieldOptions()
{
$options = $this->formField->options();
if (!$options && $this->mode === static::MODE_RELATION) {
$options = RelationBase::noConstraints(function () {
$query = $this->getRelationObject()->newQuery();
// Even though "no constraints" is applied, belongsToMany constrains the query
// by joining its pivot table. Remove all joins from the query.
$query->getQuery()->getQuery()->joins = [];
return $query->lists($this->nameFrom); // <==== LOOK HERE
});
}
return $options;
}
You can see this nameFrom is directly passed to query and query/sql do not know about our virtual field so it wont work.
Alternatively you can use RelationController Behaviors
ref: https://octobercms.com/docs/backend/relations#introduction
if any doubt please comment.

Get name instead of ID one to one relationship

I have a make table and post table. Make table saves make names as make_code and make_name.
Post table has a column make. While saving a post, it will save make in make_code.
While displaying in blade, I want it to display as make_name. How can I do it?
Currently {{$post->make}} gives me make_code. I need it to show make_name.
I think its a one-to-one relationship that's needed. I tried putting it in model but did not work. How can I achieve it?
MAKE MODEL
class Make extends Model
{
public function make()
{
return $this->belongsTo(App\Post::class);
}
}
POST MODEL:
class Post extends Model
{
protected $table = 'posts';
}
Update
As Tim Lewis noticed:
the relationships can't be named make, as that's a conflict.
Assuming that the your relationship work like this:
a Make has many Post
a Post belongs to a Make object.
| Note: Correct me if I'm wrong.
So, if this is correct, you should define your relationships like this:
Post.php
public function make_rel()
{
return $this->belongsTo(Make::class, 'make', 'make_code');
}
Make.php
public function posts()
{
return $this->hasMany(Post::class, 'make', 'make_code');
}
Check the One-to-Many and One-to-Many (Inverse) relationship sections of the documentation.
So, you could do in your controller (or wherever you want):
$post = Post::find(1);
dd($post->make_rel->make_name); // 'Harley Davidson'
Additionally, you could create a computed property as a shorcout to access this related property in your Post model:
Post.php
// ...
public function getMakeNameAttribute()
{
return $this->make_rel->make_name;
}
Now, you can access it like this:
$post = Post::find(1);
dd($post->make_name); // 'Harley Davidson'
Suggestion
As a suggestion, I strongly advice you to change your foreign key column from make to make_id (in your 'posts' table) to avoid conflicts. Also, you could relate the post to the make primmary key instead of a custom key given the fact that this link is almost invisible and it is handled by Laravel. This would speed up the execution of the query because primmary id's are indexed by default.

One To Many Relationship in Eloquent with JSON field

I'm trying to create a connection between a JSON field in my database and a table which stores music by ID. So, I have a table called "playlists" which has a field called "songs". In this "songs" field I have a array[] of song ID's e.g. [1,2]. I tried the following code to make a relationship between these two tables:
class Playlist extends Model
{
protected $table = 'playlists';
public function songs()
{
return $this->hasMany('App\Music', 'id');
}
}
I used the foreign_key id because of the songs table which has a id field.
The code I used to retrieve the playlist from the controller is as follows:
$playlist = Playlist::find($id)->songs;
print_r($playlist);
Which outputs:
[1,2]
I most probably did something wrong, not understanding the relationships correctly. Could someone explain how this works? I looked up the documentation but did not get any wiser.
Laravel has no native support for JSON relationships.
I created a package for this: https://github.com/staudenmeir/eloquent-json-relations
If you rename the songs column to song_ids, you can define a many-to-many relationship like this:
class Playlist extends Model
{
use \Staudenmeir\EloquentJsonRelations\HasJsonRelationships;
protected $casts = [
'song_ids' => 'json',
];
public function songs()
{
return $this->belongsToJson('App\Music', 'song_ids');
}
}
class Music extends Model
{
use \Staudenmeir\EloquentJsonRelations\HasJsonRelationships;
public function playlists()
{
return $this->hasManyJson('App\Playlist', 'song_ids');
}
}
Although this is a very old post but I will go ahead and drop my own opinion for my future self and fellow googlers.....
So, If I got this question correctly, you are trying to use a JSON field for a relationship query. This issue I have stumbled across a couple of times, at different occasions for different use-cases. With the most recent being for the purpose of saving a couple of Ids belonging to different tables, in a single JSON field on a given table (While I keep pondering on why the Laravel guy won't just add this functionality already! I Know Pivots, Data Normalization etc....But I'm pleading for the 1%). Until I came across this post on Laracast that worked like a charm.
Apologies for the long intro, let me get right into it....
On your Playlist model (in Laravel 8.0 and a few older versions I can't really keep track of) you can do something like so;
public function songs()
{
$related = $this->hasMany(Song::class);
$related->setQuery(
Song::whereIn('id', $this->song_ids)->getQuery()
);
return $related;
}
I have the really good solution for keeping data in column on json format. It help me on previous project online shop
https://scotch.io/tutorials/working-with-json-in-mysql

Automatically remove related models on removing selected model in laravel

I have a Lesson model with below fields :
lesson_id
title
start_date
end_date
And a Content model that have these fields :
content_id
lesson_id
contentable_id
contentable_type
order
A OneToMany relationship is between Lesson and Content.
In addition, I have two another models named Unit with these fields :
unit_id
title
time
And Test by these :
test_id
title
description
Content contentable_type and contentable_id attributes holds unit (or test) data related to a specific Lesson.
For example in contentable_type field of Content can insert only App\Unit or App\Test string and contentable_id holds ID of that Unit or Test.
(Be careful that Content Model is not to create a morph relations between these tables and I created it beacause I want to give Ordering capability to units and test of a Lesson)
Now, Suppose I want to delete a specific Content and related Units (or Tests).
Deleting Content model instance is easy, but to remove related Unit (or Test), I must fetch appropriate model name from contentable_type and it's ID then select and delete it.
For that I wrote this :
public function destroy ($course_id, $lesson_id, $content_id)
{
$content = Content::findOrFail($content_id);
$modelName = $content->contentable_type;
$modelName::find($content->contentable_id)->delete();
$content->delete();
}
Despite working properly ,But I think that is not convenient.
I search for a way that when delete a Content model , this automatically found related model and remove it too.
What is best and Proper solution?
In this case I just do these changes :
public function destroy ($course_id, $lesson_id, $content_id)
{
$content = Content::findOrFail($content_id);
$content->delete();
}
And in the Content Model :
public static function boot()
{
static::deleting(function ($content) {
$modelName = $content->contentable_type;
$modelName::find($content->contentable_id)->delete();
});
}
This works fine.
But do this best and Most correct solution?

Categories