Model with multiple polymorphic fields - php

I need to add a feature to my system, it's a stock control system.
The feature to be added is about the possibility to start chats in different models. I have Products, Sellers, ProductCollections and a lot of more objects and I need to start conversations within them.
So I thought about making a table with three polymorphic fields to make it fully reusable with any variants. This are the fields
sender_id
sender_type
topic_id
topic_type
receiver_id
receiver_type
And the proper fields to make the conversation possible (message, in_reply_of, etc...)
The sender and receiver needs to be polymorphic because a conversation could be made between SystemUsers and Customers.
Am I in the right way? Will this work in this way? Also I don't know how could I save the Chat entities.

If you want to set a chat for multiple sender, receiver and topics, I believe this relation is good to go.
Also, I was unable to to understand what you exactly mean by Chat entities but the below approach should clear out any doubts you might be having about this approach.
Below is how you can get things done!
Setting the Relations
Set the relations in the following manner
class Chat
{
public function sender()
{
return $this->morphTo();
}
public function topic()
{
return $this->morphTo();
}
public function receiver()
{
return $this->morphTo();
}
}
class SystemUser
{
public function sentChats()
{
return $this->morphMany('App\Chat', 'sender');
}
public function receivedChats()
{
return $this->morphMany('App\Chat', 'receiver');
}
}
class Customer
{
public function sentChats()
{
return $this->morphMany('App\Chat', 'sender');
}
public function receivedChats()
{
return $this->morphMany('App\Chat', 'receiver');
}
}
class Illustrate
{
public function illustrable()
{
return $this->morphTo();
}
public function chats()
{
return $this->morphMany('App\Chat', 'topic');
}
}
Creating A Chat
How to create a chat
public function create()
{
$inputs = request()->only([
'sender_id', 'sender_type', 'receiver_id', 'receiver_type', 'topic_id', 'topic_type',
'message', 'in_reply_of',
]);
$sender = $this->getSender($inputs);
$receiver = $this->getReceiver($inputs);
$topic = $this->getTopic($inputs);
if($sender && $receiver && $topic) {
$chat = $sender->sentChats()->create([
'receiver_id' => $receiver->id,
'receiver_type' => get_class($receiver),
'topic_id' => $topic->id,
'topic_type' => get_class($topic),
'message' => $inputs['message'],
'in_reply_of' => $inputs['in_reply_of'],
]);
}
}
private function getSender($inputs)
{
if(isset($inputs['sender_type'], $inputs['sender_id']) && is_numeric($inputs['sender_id'])) {
switch($inputs['sender_type']) {
case 'SystemUser':
return SystemUser::find($inputs['sender_id']);
case 'Customer':
return Customer::find($inputs['sender_id']);
default:
return null;
}
}
}
private function getReceiver($inputs)
{
if(isset($inputs['receiver_type'], $inputs['receiver_id']) && is_numeric($inputs['receiver_id'])) {
switch($inputs['receiver_type']) {
case 'SystemUser':
return SystemUser::find($inputs['receiver_id']);
case 'Customer':
return Customer::find($inputs['receiver_id']);
default:
return null;
}
}
}
private function getTopic($inputs)
{
if(isset($inputs['topic_type'], $inputs['topic_id']) && is_numeric($inputs['topic_id'])) {
switch($inputs['topic_type']) {
case 'Illustrate':
return Illustrate::find($inputs['topic_id']);
default:
return null;
}
}
}
Getting a chat
public function get($id) {
$chat = Chat::find($id);
$sender = $chat->sender;
// Inverse
// $systemUser = SystemUser::find($id);
// $systemUser->sentChats->where('id', $chatId);
$receiver = $chat->receiver;
// Inverse
// $customer = Customer::find($id);
// $customer->receivedChats->where('id', $chatId);
$topic = $chat->topic;
// Inverse
// $illustrate = Illustrate::find($id);
// $illustrate->chats;
}
Note :- Please understand I haven't tested any of this... This is just a small example on how you can get things done.
Let me know if you face any issues understanding this.

Related

Laravel nova restrict viewAny()

When I want users not to be able to enter an individual resource I can use policies to do the following:
public function view(User $user, Model $object)
{
if($user->groupName != $object->groupName) {
return false;
} else {
return true;
}
}
This has as a result that the Components of your group have the eye icon (see red cirle). Components I do not want the user to see dont have the eye icon.
My desired result is that the should not be seen component is not shown at all. How can I achieve this?
I tried:
public function viewAny(User $user)
{
// $object does not exist here so I cannot use it to filter
if($user->groupName == $object->groupName) {
return true;
} else {
return false;
}
}
You need to update the index query of your resource. see more
public static function indexQuery(NovaRequest $request, $query)
{
return $query->where('groupName', $request->user()->group_name);
}
You should consider updating the relateble query too.

Attaching another table after a hasManyThrough relationship

i am trying to connect 4 tables together:
StepAnimation Model:
public function steps()
{
return $this->hasManyThrough('App\Models\Step','App\Models\WorkSequence');
}
public function step()
{
return $this->belongsTo('App\Models\Step');
}
Step Model:
public function workSequence()
{
return $this->belongsTo('App\Models\WorkSequence');
}
public function stepAnimation()
{
return $this->hasMany('App\Models\StepAnimation');
}
WorkSequence Model:
public function categoryWorkSequence()
{
return $this->hasMany('App\Models\CategoryWorkSequence');
}
public function step()
{
return $this->hasMany('App\Models\Step');
}
CategoryWorkSequence Model:
public function workSequence()
{
return $this->belongsTo('App\Models\WorkSequence');
}
I am currently selecting all the work sequences that have a step animation:
$this->stepAnimation = New StepAnimation();
$workSequence = $this->stepAnimation::with('step.workSequence')->get();
But now id like to attach all the categories to the worksequence, how do i need to extend the select for that to happen?
Then you can continue selecting the category relationship in your workSequence as long as it is available on your model. Like this.
$this->stepAnimation = New StepAnimation();
$workSequence = $this->stepAnimation::with('step.workSequence.categoryWorkSequence')->get();

get many to many with where clause in laravel

I Have this 3 tables like below :
Tools
Parts
Part_details
it is my the table structure :
Tool -> has many -> parts. part -> has many->part_details.
Tool : id*, name; Parts : id*, name, tool_id; part_details: id, part_id, total;
Question :
Using laravel Model, how can I get Tool with One part that has biggest total on parts_details ??
// Tool Model
public function parts(){
return $this->hasMany(Part::class);
}
// Part Model
public function part(){
return $this->belongsTo(Tool::class);
}
public function part_details(){
return $this->hasMany(PartDetail::class);
}
// PartDetail Model
public function part(){
return $this->belongsTo(Part::class);
}
Now query the Tool model
$tools = Tool::with('parts')->withCount('parts.part_details')->get();
$toolWithMaxCount = $tools->filter(function($tool) use ($tools){
return $tool->parts->max('par_details_count') === $tools->max('parts.part_details_count');
})->first();
You can improve this with adding some raw bindings to optimise it. I think you got the idea.
Tool model
public function parts() {
return $this->hasMany('App\Part');
}
Part Model
public function details() {
return $this->hasMany('App\PartDetail');
}
public function tool() {
return $this->belongsToMany('App\Tool');
}
Detail Model
public function part() {
return $this->belongsToMany('App\Part');
}
Controller
$tools = Tool::with('parts', 'parts.details')
->find($id)
->max('parts.part_details');
Use the the hasManyThrough Relationship to get the all part details related to tool and then you can check the one by one record and get the highest total of the tool part.
// Tool Model
public function partsdetails()
{
return $this->hasManyThrough('App\PartDetail', 'App\Part','tool_id','part_id');
}
In Your controller
$data = Tool::all();
$array = [];
if(isset($data) && !empty($data)) {
foreach ($data as $key => $value) {
$array[$value->id] = Tool::find($value->id)->partsdetails()->sum('total');
}
}
if(is_array($array) && !empty($array)) {
$maxs = array_keys($array, max($array));
print_r($maxs);
}
else{
echo "No Data Available";
}

Laravel 5.1 code optimisation

Is there a way that the below code can be shortened? It's starting to look a bit messy and I wanted to know if there was a better way.
/**
* Update user.
*
* #param $request
* #param $id
* #return mixed
*/
public function updateUser($request, $id)
{
// Get user
$user = $this->user->find($id);
// Sync job titles
if($request->has('job_title'))
{
$user->jobTitles()->sync((array)$request->get('job_title'));
} else {
$user->jobTitles()->detach();
}
// Sync employee types
if($request->has('employee_type'))
{
$user->employeeTypes()->sync((array)$request->get('employee_type'));
} else {
$user->employeeTypes()->detach();
}
if($request->has('status')) {
$data = $request->only('first_name', 'last_name', 'email', 'status');
} else {
$data = $request->only('first_name', 'last_name', 'email');
}
// Save user changes
return $this->user->whereId($id)->update($data);
}
Any help would be appreciated.
Here is what I would have done:
Extracted the ManyToMany relationships to their own methods.
Initialized the $data variable and overridden it if necessary.
Removed the comments. The code is readable enough, no need for them
Code:
public function updateUser($request, $id)
{
$user = $this->user->find($id);
$data = $request->only('first_name', 'last_name', 'email');
$this->syncJobTitles($user);
$this->syncEmployeeTypes($user);
if($request->has('status')) {
$data['status'] = $request->status;
}
return $user->update($data);
}
private function syncJobTitles($user)
{
if(request()->has('job_title'))
{
$user->jobTitles()->sync((array) request()->get('job_title'));
} else {
$user->jobTitles()->detach();
}
}
private function syncEmployeeTypes($user)
{
if(request()->has('employee_type'))
{
$user->employeeTypes()->sync((array) request()->get('employee_type'));
} else {
$user->employeeTypes()->detach();
}
}
There are different ways to refactor that code:
If that code is only used on that part of your code you could leave it there or
Option 1
Move that business logic to the user model
class User extends Eloquent
{
...
public function applySomeRule($request)
{
if($request->has('job_title')) {
$this->jobTitles()->sync((array)$request->get('job_title'));
} else {
$this->jobTitles()->detach();
}
// Sync employee types
if($request->has('employee_type')) {
$this->employeeTypes()->sync((array)$request->get('employee_type'));
} else {
$this->employeeTypes()->detach();
}
}
}
And your controller could finish like
public function updateUser($request, $id)
{
// Get user
$user = $this->user->find($id);
$user->applySomeRule($request);
if($request->has('status')) {
$data = $request->only('first_name', 'last_name', 'email', 'status');
} else {
$data = $request->only('first_name', 'last_name', 'email');
}
// Save user changes
return $this->user->whereId($id)->update($data);
}
Option 2 If that business logic is used on different controller methods you can use Middlewares, so that you move that logic to middleware and in your route definition you use the middleware you created, let's say SomeRuleMiddleware.
Your routes would look like:
Route::put('user/{id}', [
'middleware' => 'SomeRuleMiddleware',
'uses' => 'YourController#updateUser'
]);
Option 3 You could move all your business logic to Repositories (read about Repository Pattern) and SOLID principles, that way your logic and rules will keep on Repositories and your controllers would keep clean, something like this:
class YourController extends Controller
{
protected $userRepo;
public function __construct(UserRepository $userRepo)
{
$this->userRepo = $userRepo;
}
public function updateUser($request, $id)
{
$data = $request->all();
$result = $this->userRepo->updateUser($id, $data);
return $result;
}
}

Model observer in Laravel 4, restore() triggering both restored() and updated()

I'm using an observer on models in Laravel 4 for the purposes of keeping historical records of changes made by each user. The code I'm currently using is as follows:
class BaseObserver {
public function __construct(){}
public function saving(Eloquent $model){}
public function saved(Eloquent $model){}
public function updating(Eloquent $model){}
public function updated(Eloquent $model)
{
$this->storeAuditData($model, 'update');
}
public function creating(Eloquent $model){}
public function created(Eloquent $model)
{
$this->storeAuditData($model, 'create');
}
public function deleting(Eloquent $model){}
public function deleted(Eloquent $model)
{
$this->storeAuditData($model, 'delete');
}
public function restoring(Eloquent $model){}
public function restored(Eloquent $model)
{
$this->storeAuditData($model, 'restore');
}
public function storeAuditData(Eloquent $model, $action)
{
$snapshot = array();
foreach ($model->fillable as $fieldName) {
$snapshot[$fieldName] = $model->$fieldName;
}
$auditData = new AuditData;
$auditData->model = get_class($model);
$auditData->rowId = $model->id;
$auditData->action = $action;
$auditData->user = Auth::user()->username;
$auditData->moment = date('Y-m-d H:i:s');
$auditData->snapshot = json_encode($snapshot);
$auditData->save();
}
}
This works fine, except when a restore() is performed, both the restored and updated methods get run, so I end up with two rows in the AuditData database table, when I only want one (the "restore").
Is there any way I can tell within the updated method whether the update is a restore or not, and only store the audit data if it is a stand-alone update and not a restore?
You could check if only the deleted_at column has been modified (is dirty)
public function updated(Eloquent $model)
{
if($model->isDirty($model->getDeletedAtColumn()) && count($model->getDirty()) == 1){
// only soft deleting column changed
}
else {
$this->storeAuditData($model, 'update');
}
}
While restoring, and using Laravel auto timestamps, the column updated_at will be modified along with deleted_at. To differentiate between 'restored' and 'updated' observer event, within updated method I use:
public function updated(Eloquent $model)
{
if ($model->isDirty($model->getDeletedAtColumn())
&& $model->{$model->getDeletedAtColumn()} === null
&& $model->getOriginal($model->getDeletedAtColumn()) !== null) {
// model was restored
}
}

Categories