Laravel, simplifying relationships: multiple sortable models - php

As I was working on a Laravel project I noticed a recurring pattern in my models:
Model A hasMany Model B
Model B can optionally be hidden
Model B can optionally be sorted
Examples:
A gallery has pictures. You can choose not to display a picture or you can choose to sort them.
A "view our team" page. You can choose not to display certain employees or you can choose to sort them.
A slider on the home page. You can choose not to display certain images or you can choose to sort them.
I've been implementing all of these as follows:
class ModelA extends Model {
function modelBs() {
$this->hasMany('modelB');
}
}
class modelB extends Model {
protected $fillable = ['visible', 'order'];
function modelA() {
$this->belongsTo('modelA');
}
}
I'm also repeatedly re-implementing (copying/pasting) the code for displaying these in blade templates:
#foreach( $modelA->modelBs()->sortBy('order') as $modelB )
#if( $modelB->visible )
<li>{{ $modelB->output() }}</li>
#endif
#endforeach
And in the admin panel I have to repeatedly re-implement (copy/paste) the the jQuery-UI sortable widget for modifying the order, serializing your decision, and submitting it to the server then saving this order (via updating every Model B's order appropriately)
It's getting out of hand, and I remembered the adage from Laracasts:
If you find yourself using copy and paste, there's probably a better way to do it
As I tried to think of a better solution, this is the first relationship that I imagined:
Model A hasMany SortThing
SortThing morphsTo sortable
Model B hasOne sortable
This way I know that any SortThing can be sorted or hidden, and a SortThing can reference any sortable object (pictures, employees, slider panels, etc)
The problem is doing this doesn't really make my code any more DRY:
class ModelA extends Model {
function modelBs() {
$this->hasMany('SortThing');
}
}
class modelB extends Model {
function sortable() {
$this->morphsOne('SortThing', 'sortable');
}
}
class SortThing extends Model {
protected $fillable = ['visible', 'order'];
function sortable() {
$this->morphTo();
}
}
#foreach( $modelA->modelBs()->sortBy('order') as $modelB )
#if( $modelB->visible )
<li>{{ $modelB->sortable->output() }}</li>
#endif
#endforeach
I've added an extra class and necessitated the sortable-> in my output and I'm still copying/pasting code.
Any advice on how to clean up my code would be appreciated. Still a bit of a Laravel newb.
Bonus points if the resulting relationship doesn't require me to update 18 database rows when objects are re-ordered as this could potentially lead to some ugly overhead as the lists get really long.
Update
Attempting #gpopoteur's answer below (after fixing the typo in his declaration of the renderItems function) I got the following error:
[2015-03-24 13:08:52] production.ERROR: exception 'Symfony\Component\Debug\Exception\FatalErrorException' with message 'App\Slider and App\HasSortableItemsTrait define the same property ($sortItemClass) in the composition of App\Slider. However, the definition differs and is considered incompatible. Class was composed' in /var/www/example.com/app/Slider.php:26
Slider.php looks like so:
<?php namespace App;
use Illuminate\Database\Eloquent\Model;
class Slider extends Model {
use HasSortableItemsTrait;
protected $sortItemClass = 'App\SliderPanel';
//
public function sliderable() {
return $this->morphTo();
}
public function panels() {
return $this->hasMany('App\SliderPanel');
}
public function output() {
$panels = $this->panels();
return "Test?";
}
} // Line 26
and HasSortableItemsTrait.php looks like so:
<?php namespace App;
trait HasSortableItemsTrait {
protected $sortItemClass; // Also tried $sortItemClass = ''; and $sortItemClass = null;
public function items() {
$this->hasMany($this->sortItemClass)->sortyBy('order')->where('visible', '=', true);
}
public function renderItems($htmlTag = '<li>:item</li>') {
$render = '';
foreach( $this->items() as $item ){
$render .= str_replace($item->render(), ':item', $htmlTag);
}
return $render;
}
}
Update 2
I've figured out that commenting out the following line fixes my issue:
protected $sortItemClass;
Of course I still have to make sure anything using the trait defines $sortItemClass or it will fail when calling items()
Now I'm getting a new error:
[2015-03-24 13:34:50] production.ERROR: exception 'BadMethodCallException' with message 'Call to undefined method Illuminate\Database\Query\Builder::sortBy()' in /var/www/example.com/vendor/laravel/framework/src/Illuminate/Database/Query/Builder.php:1992
I double-checked the Laravel docs and I'm like 90% sure that sortBy should be a valid method on query builders...

You could try adding everything in a trait, and then just use SortableTrait; on your Model. And then setting the class as a protected attribute of the other Sortable items that it has many.
!!! Code below is not tested !!!
Lets define a trait for the Class that has sortable Items.
trait HasSortableItemsTrait {
// define this in your class
// protected $sortItemClass
public function items() {
$this->hasMany($sortItemClass)->orderBy('order');
}
public function renderItems($htmlTag = '<li>:item</li>']) {
return $this->items->map(function($item) use ($htmlTag) {
if( $item->visible ){
return str_replace($item->render(), ':item', $htmlTag);
}
});
}
}
Another trait to use on the items that are sortable.
trait IsSortableTrait {
// define this in your class
// protected $sortItemParent
function items() {
$this->belongsTo($sortItemParent);
}
function render(){
return $this->output();
}
}
Lets do an example with a Gallery that has Sortable Photos. This is how the App\Gallery should look like:
class Gallery extends Model {
use HasSortableItemsTrait;
protected $sortItemClass = 'App\Photo';
}
And this is how the App\Photo class would look like:
class Photo extends Model {
use IsSortableTrait;
protected $sortItemParent = 'App\Gallery';
}
Then you just need to fetch the Item that has many sortable items, in this case the gallery.
$gallery = Gallery::find(1);
And in the view you just need to call the renderItems() method of the view.
{{ $gallery->renderItems() }}
I made the renderItems method be able to receive how you want to wrap what the $item->render() will be giving as output. For example, between <p></p> you just have to call the method like this:
{{ $gallery->renderItems('<p>:item</p>') }}

Related

Laravel hasOne relation with where clause

I have a model called RealEstate, this model has a relation with another model called TokenPrice, I needed to access the oldest records of token_prices table using by a simple hasOne relation, So I did it and now my relation method is like following:
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasOne;
class RealEstate extends Model
{
public function firstTokenPrice(): HasOne
{
return $this->hasOne(TokenPrice::class)->oldestOfMany();
}
}
By far it's fine and no complexity. But now, I need to involve another relation into firstTokenPrice.
Let me explain a bit more:
As my project grown, the more complexity was added it, like changing firstTokenPrice using by a third table called opening_prices, so I added a new relation to RealEstate called lastOpeningPrice:
public function lastOpeningPrice(): HasOne
{
return $this->hasOne(OpeningPrice::class)->latestOfMany();
}
So the deal with simplicity of firstTokenPrice relation is now off the table, I want to do something like following every time a RealEstate object calls for its firstTokenPrice:
Check for lastOpeningPrice, if it was exists, then firstTokenPrice must returns a different record of token_price table, otherwise the firstTokenPrice must returns oldestOfMany of TokenPrice model.
I did something like following but it's not working:
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasOne;
class RealEstate extends Model
{
public function lastOpeningPrice(): HasOne
{
return $this->hasOne(OpeningPrice::class)->latestOfMany();
}
public function firstTokenPrice(): HasOne
{
$lop = $this->lastOpeningPrice;
if ($lop) {
TokenPriceHelper::getOrCreateFirstToken($this, $lop->amount); // this is just a helper function that inserts a new token price into `token_prices` table, if there was none exists already with selected amount
return $this->hasOne(TokenPrice::class)->where('amount', $lop->amount)->oldestOfMany();
}
return $this->hasOne(TokenPrice::class)->oldestOfMany();
}
}
I have checked the $this->hasOne(TokenPrice::class)->where('amount', $lop->amount)->oldestOfMany() using by ->toSql() method and it returns something unusual.
I need to return a HasOne object inside of firstTokenPrice method.
You can use ofMany builder for that purpose:
public function firstTokenPrice(): HasOne
{
$lop = $this->lastOpeningPrice;
if ($lop) {
TokenPriceHelper::getOrCreateFirstToken($this, $lop->amount); // this is just a helper function that inserts a new token price into `token_prices` table, if there was none exists already with selected amount
return $this->hasOne(TokenPrice::class)->ofMany([
'id' => 'min',
], function ($query) use ($lop) {
$query->where('amount', $lop->amount);
});
}
return $this->hasOne(TokenPrice::class)->oldestOfMany();
}
I used ->oldest() with a custom scope called amounted in TokenPrice model:
class TokenPrice extends Model
{
public function scopeAmounted(Builder $query, OpeningPrice $openingPrice): Builder
{
return $query->where('amount', $openingPrice->amount);
}
/....
}
And then changed my firstTokenPrice
public function firstTokenPrice(): HasOne
{
$lop = $this->lastOpeningPrice;
if ($lop) {
TokenPriceHelper::getOrCreateFirstToken($this, $lop->amount);
return $this->hasOne(TokenPrice::class)->amounted($lop)->oldest();
}
return $this->hasOne(TokenPrice::class)->oldestOfMany();
}
It's working, but I don't know if it's the best answer or not

Laravel retrieving eloquent nested eager loading

In my application i have 4 models that relate to each other.
Forms->categories->fields->triggers
What I am trying to do is get the Triggers that refer to the current Form.
Upon researching i found nested eager loading, which would require my code to look like this
Form::with('categories.fields.triggers')->get();
Looking through the response of this i can clearly see the relations all the way down to my desired triggers.
Now the part I'm struggling with is only getting the triggers, without looping through each model.
The code i know works:
$form = Form::findOrFail($id);
$categories = $form->categories;
foreach ($categories as $category) {
$fields = $category->fields;
foreach ($fields as $field) {
$triggers[] = $field->triggers;
}
}
I know this works, but can it be simplified? Is it possible to write:
$form = Form::with('categories.fields.triggers')->get()
$triggers = $form->categories->fields->triggers;
To get the triggers related? Doing this as of right now results in:
Undefined property: Illuminate\Database\Eloquent\Collection::$categories
Since it is trying to run the $form->categories on a collection.
How would i go about doing this? Do i need to use the HasManyThrough relation on my models?
My models
class Form extends Model
{
public function categories()
{
return $this->hasMany('App\Category');
}
}
class Category extends Model
{
public function form()
{
return $this->belongsTo('App\Form');
}
public function fields()
{
return $this->hasMany('App\Field');
}
}
class Field extends Model
{
public function category()
{
return $this->belongsTo('App\Category');
}
public function triggers()
{
return $this->belongsToMany('App\Trigger');
}
}
class Trigger extends Model
{
public function fields()
{
return $this->belongsToMany('App\Field');
}
}
The triggers run through a pivot table, but should be reachable with the same method?
I created a HasManyThrough relationship with unlimited levels and support for BelongsToMany:
Repository on GitHub
After the installation, you can use it like this:
class Form extends Model {
use \Staudenmeir\EloquentHasManyDeep\HasRelationships;
public function triggers() {
return $this->hasManyDeep(Trigger::class, [Category::class, Field::class, 'field_trigger']);
}
}
Form::with('triggers')->get();
Form::findOrFail($id)->triggers;

Laravel - Using controller function for two different eloquent models

I am currently developing an application for an indy movie production company. The way I have the workflow right now, the user begins by creating a new movie object by entering the movie title and synopsis. From there the user can then add more details such as price, run-time, full-screen/wide-screen, etc. The movie basic (title, synopsis) are in one database table, and the details are in another. I have set up a one-to-one relationship between the two eloquent models. I have also set up a MovieController that allows me to very easily do CRUD operations on the movie basic model, and when I am displaying the movie object to the user, I can display both the basics and details.
What I was wondering was there some way to use the already existent functions in the movie controller to do CRUD operations on the movie details without having to create new functions in the controller? Also is it possible to reuse the views I've created for each corresponding CRUD operation? In other words can I would like
something.dev/cms/create
In one instance to match to creating a new movie (title, synopsis) and in another instance to match to creating the movie detail (price, run-time, full-screen/widescreen) etc. Is this possible? I have provide the code for the two models below:
Movie_basic.php
<?php namespace App;
use Illuminate\Database\Eloquent\Model;
class Movie_basic extends Model {
protected $fillable = ['movie_title', 'movie_synopsis'];
protected $guarded = ['id'];
public function details()
{
return $this->hasOne('App\Movie_detail', 'movie_id');
}
public function personnel()
{
return $this->hasMany('App\Movie_personnel', 'movie_id');
}
}
Model_detail.php
<?php namespace App;
use Illuminate\Database\Eloquent\Model;
class Movie_detail extends Model {
protected $fillable = ['minutes', 'languages', 'viewer_discretion', 'screen_type', 'price'];
protected $guarded = ['id', 'movie_id'];
public function basics()
{
return $this->belongsTo('App\Movie_basic');
}
}
If I understand you, this might be an answer. (Did not test the code.)
Please note that, that code has been written to show you an example. You will probably want to edit it to make it work and act as you wanted. Maybe you want to use a repository or automate the model instance creating (I did not create new instances), and saving processes. You can use interfaces instead of your models etc...
Here is the service to store the logic.
<?php
use Movie_basic; use Movie_detail;
Class MovieService {
protected $movieBasic;
protected $movieDetail;
public function __construct(Movie_basic $movieBasic, Movie_detail $movieDetail) {
$this->movieBasic = $movieBasic;
$this->movieDetail = $movieDetail;
}
public function createMovie(array $attr) {
// TODO: Move your business logic here.
// E.g
$movie = $this->movieBasic->fill($attr);
$movie->save();
return $movie;
}
public function createMovieDetail(array $movieAttr, array $attributes) {
// TODO: Move your detail logic here.
// E.g.
$basic = $this->createMovie($movieAttr);
$detail = $this->movieDetail->fill($attributes);
$detail->basic()->associate($detail);
$detail->save();
return $detail;
}
}
And here, the controller examples:
<?php
use MovieService;
class MovieController {
public function __construct(MovieService $ms) {
$this->ms = $ms;
}
public function store() {
$this->ms->createMovie($attrToSave);
}
}
<?php
use MovieService;
class MovieDetailController {
public function __construct(MovieService $ms) {
$this->ms = $ms;
}
public function store() {
$this->ms->createMovieDetail($attrToSave);
}
}

In my view, why does Laravel overwrite variables I declare in my controller with those from my model?

Am I understanding the MVC design pattern incorrectly? Why does Laravel seemingly overwrite variables I declare in my controller and pass to my view with those from my model? Say I'm passing the variable $journey from my controller to my view like so:
class JourneyController extends BaseController {
public function journey($id) {
$journey = Journey::find($id);
// I overwrite one of the attributes from the database here.
$journey->name = "Overwritten by the Controller";
return View::make('journey', array(
'journey' => $journey,
'bodyClass' => 'article'
));
}
}
But, I'm using an accessor to also modify the $journey->name attribute in my Journey model:
class Journey extends Eloquent {
protected $table = 'journeys';
protected $primaryKey = 'id';
public $timestamps = false;
public function getNameAttribute($value) {
return 'Overwritten by the Model';
}
}
So when my view is created, and I display $journey->name like so:
{{ $journey->name }}
I'm left with:
"Overwritten by the Model";
Why does this occur? Doesn't the controller handle a request, fetch information from my model, manipulate it, and then pass it to the view where it can be outputted? If this is the case, why, and also how, is the model seemingly 'jumping' in between to overwrite my controller-written variable with its own?
I know this is old, but I just found a solution on Laravel 4.2 today.
class Journey extends Eloquent {
protected $table = 'journeys';
protected $primaryKey = 'id';
public $timestamps = false;
public function getNameAttribute($value = null) {
if($value)
return $value;
return 'Overwritten by the Model';
}
}
You should update your getNameAttribute function as above to return the set value (if there is one) instead of always returning the string. Previously, calling this value would always run the function and ignore the set value, but now the function takes checks first for the value that you have set.
Hopefully 2 years isn't too late to still help some people!
Have a look at using Presenters, Take Jeffery Way's Presenter Package. Install it normally and then you can add the $presenter variable to your model.
For instance:
use Laracasts\Presenter\PresentableTrait;
class Journey extends Eloquent {
use PresentableTrait;
protected $presenter = "JourneyPresenter";
}
Then you can create your JourneyPresenter Class:
class JourneyPresenter {
public function journeyName()
{
return "Some Presentation Name";
}
}
In your view you can call this, like so:
<h1>Hello, {{ $journey->present()->journeyName }}</h1>
It really helps keep this sort of "presentation" logic out of your view and controller. You should try hard to keep your controller solely for its intended purpose, handling routes and basic guards and keep your views logic-less.
As for your problem, you may just be experiencing the natural order of Laravel operations.

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