Laravel eloquent with multiple inner joins - php

I currently am doing a raw sql query however this is causing issues with relationships and model boot methods.
Is it possible to do the following SQL query but with laravel eloquent models by relationship? Note all db tables have FK's defined, and relationships either HasOne or HasMany relationships.
$timeBreakDown = DB::select(
"SELECT
Entries.`task_id`,
Entries.`opportunity_id`,
SUM(Entries.`total_duration`) as 'duration',
Class.`class` as 'class',
Subclass.`sub_class` as 'subclass'
from entries Entries
INNER JOIN `tasks` Task
ON task_id = Task.id
INNER JOIN `task_class` Class
ON Task.`class_id` = Class.`id`
INNER JOIN `task_subclasses` Subclass
ON Task.`subclass_id` = Subclass.`id`
WHERE Entries.`opportunity_id` = '".$opportunity->id."'
GROUP BY Entries.`task_id`"
);
Models are
Entries
Tasks
Class
Subclass
How would I have to structure my models relationships to handle the above sql query?

You can write a query in this way:
Please check table names according to your database
DB:: table('table name')->join('tasks','task_id','=','tasks.id')
->join('task_class', 'tasks.subclass_id','=','task_class.id')
->join('task_subclasses','tasks.subclass_id','=', 'task_subclasses.id')
->selectRaw('entries.task_id,
task_subclasses.opportunity_id,
SUM(entries.total_duration) as duration,
task_class.class as class,
task_subclasses.sub_class as subclass')
->where(['entries.opportunity_id'=>$opportunity->id])
->groupBy('enteries.task_id')->get();

Models\Entries.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Entries extends Model
{
public function Tasks(){
return $this->hasOne(Tasks::class);
}
public function Class(){
return $this->hasMany(Classes::class);
}
public function SubClasses(){
return $this->hasOne(SubClasses::class);
}
}
Models\Tasks.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Tasks extends Model
{
public function Entries(){
return $this->belongsTo(Entries::class, "id", "task_id");
}
}
Models\Classes.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Classes extends Model
{
public function Entries(){
return $this->belongsTo(Entries::class, "class_id", "id");
}
}
Models\Subclasses.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class SubClasses extends Model
{
public function Entries(){
return $this->belongsTo(Entries::class, "id", "subclass_id");
}
}
Query:
Entries::with([
"Tasks",
"Classes",
"SubClasses"
])
->where("opportunity_id", $opportunity->id)
->groupBy("task_id")
->get();

Yes, You can do it with Eloquent I'll share an example with you
I can't read your Mess Query sorry for this but I will suggest you to do this
Entries::with(['Tasks','Class','Subclass'])->get();
from this, you will get all objects from this array
Let just say
The class have a relation with another Model but not Entries table then
the Eloquent is something like this
Entries::with(['Tasks','Class.Subclass'])->get();
hope its helpful for you

Might be something like this:
$timeBreakDown = Entries::select('entries.task_id, entries.opportunity_id', DB:raw('SUM(Entries.total_duration) as duration), task_class.class, task_subclasses.sub_class as subclass)
join('tasks', [['tasks.id', 'entries.task_id']])
join('task_class', [['task_class.id', 'entries.class_id']])
join('task_subclasses', [['task_subclasses.id', 'entries.subclass_id']])
->where('entries.opportunity_id', $opportunity->id)
->groupBy('entries.task_id')
->get();

Try this query:
$timeBreakDown = Entries::join('tasks', 'tasks.id', '=', 'entries.task_id')
->join('class', 'class.id', '=', 'entries.class_id')
->join('subclass', 'subclass.id', '=', 'entries.subclass_id')
->select(
'entries.task_id',
'entries.opportunity_id',
\DB::raw('SUM(entries.total_duration) as duration'),
'class.class',
'subclass.sub_class as subclass')
->where('entries.opportunity_id', $opportunity->id)
->groupBy('entries.task_id')
->get();
And try dd($timeBreakDown->toSql()); to match with your Raw SQL query.

From the official documentation.
You can define relationships using the base database relationship type by adding the tasks method to the Entries model.
The tasks method should call the hasOne method and return its result.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Entries extends Model
{
/**
* Get the phone associated with the user.
*/
public function task()
{
return $this->hasOne(Tasks::class);
}
}
In turn, the Tasks model will have an entry method with which we can determine the inverse of the hasOne relationship using the belongsTo method:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Tasks extends Model
{
/**
* Get the user that owns the phone.
*/
public function entry()
{
return $this->belongsTo(Entries::class);
}
}

you need to just setup the relationships like this:
I am assuming that that a Class will have a subClass and a Class will also have Tasks and those Tasks have Entries.
Also do you not have a User model?
Class Model
class Class extends Model
{
protected $with = ['entries', 'subclass', 'task'];
public function entries()
{
return $this->hasManyThrough(\App\Models\Entries::class, \App\Models\Task::class);
}
public function subClass()
{
return $this->hasOne(\App\Models\subClass::class);
}
public function tasks()
{
return $this->hasMany(\App\Models\Task::class);
}
}
Entry Model
class Entry extends Model
{
protected $with = ['task'];
public function task()
{
return $this->belongsTo(Task::class);
}
}
SubClass Model
class SubClass extends Model
{
protected $with = ['class'];
public function class()
{
return $this->belongsTo(\App\Models\Class::class);
}
}
Task Model
class Task extends Model
{
protected $with = ['entries', 'class'];
public function entries()
{
return $this->hasMany(\App\Models\Class::class);
}
public function class()
{
return $this->hasMany(\App\Models\Task::class);
}
}
With all of that set up you should be good to do something like this fro wherever your at in the stack:
$entry = Entry::findOrFail('id');
$entry->task->class->subClass->name;
or
$class = Class::findOrFail($class->id);
$subclass_name = $class->subclass->name;
$entries = $class->tasks->entries;

If you would have posted your models that would have been easier for us. But here is what I got from your raw query above.
$timeBreakDown = Entries::where('opportunity_id',$opportunity->id)->load('Tasks','Class.SubClass')->get();
You should read about Laravel Eloquent and relationships. Just for brief intro the difference between load and with used by Waleed is:
Load is used for lazy loading of relationship data while with is used for eager loading.
Eager loading is all the data gets load as soon as the Eloquent
queries the data while lazy loading loads the data when it is required.

Related

Laravel 5.4 Relations are not loading inside Model boot method

I have a model named 'Poll'. Inside Poll model I defined a boot method like follows:
public static function boot()
{
parent::boot();
self::created(function($model){
// dd($model);
$speakers = $model->speakers()->get();
// dd($speakers);
// What I want to do here is: create poll options relation from speakers as follows
// $poll->poll_options()->create([
// 'option' => $speaker->name,
// ]);
}
}
I am adding the speakers relation and it is working perfect.
But inside this boot method, inside self::created if I tried to get the speakers relation, it is always empty (dd($speakers) line). Is it because of the boot method runs just after the model is saved into DB and the relations not at all saved?
I am getting newly created model in the line: dd($model) mentioned in the code.
UPDATE
I tried with events also.
My Poll Model:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Backpack\CRUD\CrudTrait;
use Cookie;
use App\Events\PollCreated;
class Poll extends Model
{
........
protected $events = [
'created' => PollCreated::class,
];
.......
public function speakers()
{
return $this->belongsToMany('App\Models\Speaker','poll_speaker','poll_id','speaker_id');
}
}
app/Events/PollCreated.php:
namespace App\Events;
use App\Models\Poll;
use Illuminate\Queue\SerializesModels;
class PollCreated
{
use SerializesModels;
public $poll;
/**
* Create a new event instance.
*
* #param Poll $poll
* #return void
*/
public function __construct(Poll $poll)
{
// $this->poll = $poll;
$event = $poll->event()->first();
// dd($event);
// dd($poll->speakers()->get());
// dd($poll->load('speakers'));
}
}
Here also I am not getting speakers, in the line: dd($poll->speakers()->get());
my Speaker model:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Backpack\CRUD\CrudTrait;
class Speaker extends Model
{
use CrudTrait;
……..
public function polls()
{
return $this->belongsToMany('App\Models\Poll');
}
……..
}
The problem is with timing as models must always be created before they can be set in a many-to-many relationship. So there is no possible way that in a many-to-many relationship during the created event the relationship is already set as the created events are always raised before the relationships.
Anyone looking for a solution can probably experiment with the chelout/laravel-relationship-events package as this adds relationship events to models.
To be sure, I tested this out with a simple application of users and computers.
User.php
class User extends Model
{
use HasBelongsToManyEvents;
public static function boot() {
parent::boot();
self::created(function($model){
Log::info('user::created');
});
static::belongsToManyAttaching(function ($relation, $parent, $ids) {
$ids = implode(' & ', $ids);
Log::info("Attaching {$relation} {$ids} to user.");
});
static::belongsToManyAttached(function ($relation, $parent, $ids) {
$ids = implode(' & ', $ids);
Log::info("Computers {$ids} have been attached to user.");
});
}
public function computers() {
return $this->belongsToMany(Computer::class, 'user_computers');
}
}
Computer class is the same in reverse. And for the following code:
$user = User::create();
$user->computers()->attach([
Computer::create()->id,
Computer::create()->id
]);
This was the outcome:
user::created
computer::created
computer::created
Attaching computers 69 & 70 to user.
Computers 69 & 70 have been attached to user.

I want to write this SQL in Laravel Eloquent -select all author names belongs to a book

I want to write this SQL in Laravel Eloquent
SELECT book.book_id,book.book_name,author.author_name,book.price,book.publication
FROM author,book,book_author
WHERE bookauthor.b_id=book.book_id and bookauthor.a_id=author.author_id
There's a better approach using the eloquent relationships. The Book model should have defined the following relationship:
namespace App;
use Illuminate\Database\Eloquent\Model;
class Book extends Model
{
/**
* Get the authors for the book.
*/
public function authors()
{
return $this->hasMany('App\Author');
}
}
and the authors should belong to the book:
namespace App;
use Illuminate\Database\Eloquent\Model;
class Author extends Model
{
/**
* Get the book of the author.
*/
public function book()
{
return $this->belongsTo('App\Book');
}
}
If you want to get only the authors names you can use the reduce method on the authors collection:
$authors = App\Book::find(1)->authors;
$authorsNames = $authors->reduce(function ($carry, $author) {
$carry[] = $author->name;
return $carry;
}, array());
Use Laravel Joins:
bookauthor::Join('book', 'book.book_id', '=', 'bookauthor.b')
->Join('author','bookauthor.a_id','=','author.author_id')
->select('book.book_id','book.book_name','author.author_name','book.price','book.publication')
->get();
Try this. Good luck :)

How can I compact this code for sending info to the view from the Laravel Controller?

Here's my edit function in the controller
public function edit($id)
{
$game = Game::find($id);
// build list of team names and ids
$allTeams = Team::all();
$team = [];
foreach ($allTeams as $t)
$team[$t->id] = $t->name();
// build a list of competitions
$allCompetitions = Competition::all();
$competition = [];
foreach ($allCompetitions as $c)
$competition[$c->id] = $c->fullname();
return View::make('games.edit', compact('game', 'team', 'competition'));
}
I am sending data in order to display in a select list. I know about Eloquent ORM method Lists, but the problem is as far as I know it can only take property names as an argument, and not methods (like name() and fullname()).
How can I optimize this, can I still use Eloquent?
I would look into attributes and appends. You can do what you would like by adjusting your models.
Competition
<?php namespace App;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
class Competition extends Model
{
protected $appends = ['fullname'];
...
public function getFullnameAttribute()
{
return $this->name.' '.$this->venue;
}
}
Team
<?php namespace App;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
class Team extends Model
{
protected $appends = ['name'];
...
public function getNameAttribute()
{
return $this->city.' '.$this->teamName;
}
}
Controller
public function edit($id)
{
$game = Game::find($id);
$team = Team::get()->lists('id','name');
$competition = Competition::get()->lists('id','fullname');
return View::make('games.edit', compact('game', 'team', 'competition'));
}
The only thing I can think of (aside from using the map functionality of Eloquent collections) is to overwrite the toArray method in your model to add some custom attributes.
Eg.
public function toArray()
{
return array_merge(parent::toArray(), [
'fullname' => $this->fullname(),
]);
}
This will allow you to use something like:
$competition = $allCompetitions->fetch('fullname');
Although:
In saying all this I think the more elegant solution is to just provide the whole competition objects to the view and let the loop where you render them (or whatever) call the method itself.
You can call model method in view file if they are not related with other models. So if name() & fullname() returns result related to this model then you can use this model methods in view
#foreach (($allTeams as $t)
{{ $t->name() }}
#endforeach
ofcourse you have to pass the $allteams collection from controller to view

Accessors (Getter) & Mutators (Setter) On a Pivot Table in Laravel

I have a pivot table that connects users to workspaces. On the pivot table, I also have a column for role, which defines the users role for that workspace. Can I provide Accessor (Getter) & Mutator (Setter) methods on the role inside the pivot table? I have been trying to look all over, but details on pivot tables in eloquent are pretty sparse.
I am not sure if I have to setup a custom pivot model? If I do, an example would be awesome as the documentation on pivot models is very basic.
Thanks.
If all you need to do is access additional fields on the pivot table, you just need to use the withPivot() method on the relationship definition:
class User extends Model {
public function workspaces() {
return $this->belongsToMany('App\Models\Workspace')->withPivot('role');
}
}
class Workspace extends Model {
public function users() {
return $this->belongsToMany('App\Models\User')->withPivot('role');
}
}
Now your role field will be available on the pivot table:
$user = User::first();
// get data
foreach($user->workspaces as $workspace) {
var_dump($workspace->pivot->role);
}
// set data
$workspaceId = $user->workspaces->first()->id;
$user->workspaces()->updateExistingPivot($workspaceId, ['role' => 'new role value']);
If you really need to create accessors/mutators for your pivot table, you will need to create a custom pivot table class. I have not done this before, so I don't know if this will actually work, but it looks like you would do this:
Create a new pivot class that contains your accessors/mutators. This class should extend the default Pivot class. This new class is the class that is going to get instantiated when User or Workspace creates a Pivot model instance.
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\Pivot;
class UserWorkspacePivot extends Pivot {
getRoleAttribute() {
...
}
setRoleAttribute() {
...
}
}
Now, update your User and Workspace models to create this new pivot table class, instead of the default one. This is done by overriding the newPivot() method provided by the Model class. You want to override this method so that you create an instance of your new UserWorkspacePivot class, instead of the default Pivot class.
class User extends Model {
// normal many-to-many relationship to workspaces
public function workspaces() {
// don't forget to add in additional fields using withPivot()
return $this->belongsToMany('App\Models\Workspace')->withPivot('role');
}
// method override to instantiate custom pivot class
public function newPivot(Model $parent, array $attributes, $table, $exists) {
return new UserWorkspacePivot($parent, $attributes, $table, $exists);
}
}
class Workspace extends Model {
// normal many-to-many relationship to users
public function users() {
// don't forget to add in additional fields using withPivot()
return $this->belongsToMany('App\Models\User')->withPivot('role');
}
// method override to instantiate custom pivot class
public function newPivot(Model $parent, array $attributes, $table, $exists) {
return new UserWorkspacePivot($parent, $attributes, $table, $exists);
}
}
I figured out how to use Accessors and Mutators on the Pivot table (I'm using Laravel 5.8)
You must use using() on your belongsToMany relationships, for example:
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model {
public function workspaces() {
return $this->belongsToMany('App\Workspace')->using('App\UserWorkspace');
}
}
namespace App;
use Illuminate\Database\Eloquent\Model;
class Workspace extends Model {
public function users() {
return $this->belongsToMany('App\User')->using('App\UserWorkspace');
}
}
So, use your Pivot model:
namespace App;
use Illuminate\Database\Eloquent\Relations\Pivot;
class UserWorkspace extends Pivot {
public function getRoleAttribute() {
// your code to getter here
}
public function setRoleAttribute($value) {
// your code to setter here
}
}
This is a difficult question. The solutions I can think of are smelly and may cause some problems later on.
I am going to extend on Patricus's answer to make it work.
I was going to comment on Patricus's answer but there is simply too much to explain. To make his solution work with attach and sync we must do some ugly things.
The Problem
First let's identify the problem with his solution. His getters and setters do work but the belongsToMany relationship doesn't use the Pivot model when running sync, attach, or detach. This means every time we call one of these with the $attributes parameter the non-mutated data will be put into the database column.
// This will skip the mutator on our extended Pivot class
$user->workspaces()->attach($workspace, ['role' => 'new role value']);
We could just try to remember that every time we call one of these we can't use the second parameter to attach the mutated data and just call updateExistingPivot with the data that must be mutated. So an attach would be what Patricus stated:
$user->workspaces()->attach($workspace);
$user->workspaces()->updateExistingPivot($workspaceId, ['role' => 'new role value']);
and we could never use the correct way of passing the pivot attributes as the attach methods second parameter shown in the first example. This will result in more database statements and code rot because you must always remember not to do the normal way. You could run into serious problems later on if you assume every developer, or even yourself, will just know not to use the attach method with the second parameter as it was intended.
The Solution (untested and imperfect)
To be able to call attach with the mutator on the pivot columns you must do some crazy extending. I haven't tested this but it may get you on the right path if you feel like giving it a try. We must first create our own relationship class that extends BelongsToMany and implements our custom attach method:
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class UserWorkspaceBelongsToMany extends BelongsToMany {
public function attach($id, array $attributes = [], $touch = true)
{
$role = $attributes['role'];
unset($attributes['role']);
parent::attach($id, $attributes, $touch);
$this->updateExistingPivot($id, ['role' => $role], $touch);
}
// You will need sync here too
}
Now we have to make each Model::belongsToMany use our new UserWorkspaceBelongsToMany class instead of the normal BelongsToMany. We do this by mocking the belongsToMany in our User and Workspace class:
// put this in the User and Workspace Class
public function userWorkspaceBelongsToMany($related, $table = null, $foreignKey = null, $otherKey = null, $relation = null)
{
if (is_null($relation)) {
$relation = $this->getBelongsToManyCaller();
}
$foreignKey = $foreignKey ?: $this->getForeignKey();
$instance = new $related;
$otherKey = $otherKey ?: $instance->getForeignKey();
if (is_null($table)) {
$table = $this->joiningTable($related);
}
$query = $instance->newQuery();
return new UserWorkspaceBelongsToMany($query, $this, $table, $foreignKey, $otherKey, $relation);
}
As you can see, we are still calling the database more but we don't have to worry about someone calling attach with the pivot attributes and them not getting mutated.
Now use that inside your models instead of the normal belongsToMany:
class User extends Model {
public function workspaces() {
return $this->userWorkspaceBelongsToMany('App\Models\Workspace')->withPivot('role');
}
}
class Workspace extends Model {
public function users() {
return $this->userWorkspaceBelongsToMany('App\Models\User')->withPivot('role');
}
}
Its impossible to use setters, will not affect pivot table... make the change in the controller instead.

Like or favorite a post with unique user using Laravel Eloquent

I was creating a like system for my website. in this I wanted one user can only like one time for a post. and a post can be liked by many user. Also many user can like many post.
So if I guess it right, It is a many to many reletionship.
in this context,
I create the following table
... users table:
id
name
....
posts table :
id
post
...post_likes table
id
user id
poost_id
Now I am having the following model for
user :
class User extends SentryUserModel {
public function post_likes()
{
return $this->has_many('Post_like', 'id');
}
}
post :
class Post extends Eloquent {
public function post_likes()
{
return $this->has_many('Post_like', 'id');
}
}
post_like :
class Post_like extends Eloquent {
public function posts()
{
return $this->belongs_to('Post', 'post_id');
}
public function users()
{
return $this->belongs_to('User', 'user_id');
}
}
now when I am going to insert into the database (for post_likes table) I am getting an error called
Illuminate \ Database \ Eloquent \ MassAssignmentException
user_id
Also I want to know is there any way to inset into database like
$user->like()->save($user); ?????
Thank you in advance. Happy coding . \m/
I'll start with a basic issue, firstly you might want to make sure all your tables are lower case (still as a snake case as well), it's not required but it's ultimately how it's expected to be with Laravel so it makes life easier to keep with that. Also a note to the wise, like Class names, database tables are typically in the singular so user instead of users
Secondly yes you can do an insert with $user->post_likes()->save($debate); as your post_likes method on the user class returns has_many.
Thirdly, your design of the Post_like class is a bit off, you could be better off make it like so:
class PostLike extends Eloquent { // note that PostLikes is a more standard naming for a class, they should ideally be camel case names but with all capitals for words
protected $table = 'post_like'; // specifies the table the model uses
public function post() // this should be singular, the naming of a belngs_to method is important as Laravel will do some of the work for you if let it
{
return $this->belongs_to('Post'); // by naming the method 'post' you no longer need to specify the id, Laravel will automatically know from the method name and just adding '_id' to it.
}
public function users()
{
return $this->belongs_to('User');
}
}
Fourthly, your other classes could be better as:
class Post extends Eloquent {
public function post_likes()
{
return $this->has_many('PostLike'); // doesn't require you to specify an id at all
}
}
I can't exactly tell you why you're getting that mass assign error, your post is a bit garbled and doesn't look like you've included the code that actually causes the exception? I have a feeling though is that you're trying to do an insert for multiple database rows at one time but haven't defined a fillable array for PostLike such as with here: http://four.laravel.com/docs/eloquent#mass-assignment
According to Laravel 5:
User Model:
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model {
public function post_likes()
{
return $this->hasMany('App\PostLike');
}
}
Post Model:
namespace App;
use Illuminate\Database\Eloquent\Model;
class Post extends Model {
public function post_likes()
{
return $this->hasMany('App\PostLike');
}
}
PostLike Model:
namespace App;
use Illuminate\Database\Eloquent\Model;
class PostLike extends Model {
protected $table = 'post_like';
public function posts()
{
return $this->belongsTo('App\Post');
}
public function users()
{
return $this->belongsTo('App\User');
}
}
and if you want to save the post_like data then:
$inputs['post_id'] = 1;
$inputs['user_id'] = 4;
$post_like = PostLike::create($inputs);
Hope this helps!!

Categories