Laravel Eloquent Model inheritance - php

I'm a new bit who is trying to build an app using Laravel 5.5, and the Eloquent model.
I have two classes: (1)
Customer
and (2)
VIPCustomer which extends Customer.
You may immediately tell VIPCustomer contains all attributes that a customer has, and other extra attributes.
Just to be clear, a customer may not be a VIP, and a VIP must be a customer; The customer may immediately opt-in to be a VIP the first time he shops.
Therefore, I am attempting to do something like this in the database:
Customer:
+------------------------+
|id|name|gender|join_date|
+------------------------+
VIPCustomer:
+----------------------------------+
|customer_id|valid_until|type|point|
+----------------------------------+
(customer_id is a foriegn key referencing Customer.id)
And accordingly, in the model php file:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Customer extends Model
{
}
.
namespace App;
use Illuminate\Database\Eloquent\Model;
class VIPCustomer extends Customer
{
public $incrementing = false;
}
And that's it? I saw there are others saying I should using polymorphic relationship but I don't understand what it means.
In addition, is it possible to do instantiate a new VIP Customer something like this?
$customer = new VIPCustomer;
$customer->name = 'Alice';
$customer->gender = 'F';
$customer->type = 'gold';
$customer->point = 0;
$customer->save();
On top of that, say when the VIP membership ends, is it possible to preserve that person as Customer? Because I'm afraid deleting that person will delete him from both Customer and VIPCustomer tables.
Thank you very much in advance.

Your current VIPCustomer class looks like a class that holds a VIP data, not a subject (a customer). Then so, I would rename it as VIPCustomerData here and make a new VIPCustomer to inherit Customer class instead.
class Customer extends Model
{
protected $table = 'customers';
}
Make sure you define the table name to avoid it being guessed by inheritance. Then tell VIPCustomer to has a relation to VIPCustomerData.
class VIPCustomer extends Customer
{
public function vipData()
{
return $this->hasOne(VIPCustomerData::class, 'customer_id', 'id');
}
}
Now, the problem is whenever you're going to retrieve VIP customers like VIPCustomer::get(), you'll get whole customers instead. So, applying global scope is needed.
class VIPCustomer extends Customer
{
protected static function boot()
{
parent::boot();
static::addGlobalScope('weareviptypeofcustomer', function ($q) {
$q->has('vipData'); // only customers with vip data
});
}
public function vipData()
{
return $this->hasOne(VIPCustomerData::class, 'customer_id', 'id');
}
}
To create a new Customer as VIP, of course 2 queries is needed to insert here. Example,
$vipCustomer = new VIPCustomer;
$vipCustomer->name = 'Alice';
$vipCustomer->gender = 'F';
$vipCustomer->save();
$vipCustomerData = new VIPCustomerData;
$vipCustomerData->type = 'gold';
$vipCustomerData->point = 0;
$vipCustomer->vipData()->save($vipCustomerData);
Example of updating point.
$vipCustomerData = $vipCustomer->vipData; // or $vipCustomer->vipData()->first();
$vipCustomerData->point = 10;
$vipCustomerData->save();
Example of removing VIP status from customer. Of course just delete VIPCustomerData from its table.
$vipCustomer->vipData()->delete();
However, it's better to maintain these subjects as one class if there is no special column to treat each subject differently.
class Customer extends Model
{
protected $table = 'customers';
protected $with = ['vipData']; // always eager load related 'vipData'
protected $appends = ['is_vip']; // append 'is_vip' accessor
public function vipData()
{
return $this->hasOne(static::class, 'customer_id', 'id');
}
public function getIsVipAttribute()
{
return (bool) $this->vipData;
}
}
$customers = Customer::all();
foreach($customers as $customer) {
if ($customer->is_vip) {
// is VIP
} else {
}
}

Related

Model access in Laravel > 5

First of all, I'm new to Laravel. I come from Codeigniter where you can have something similar to:
class Test_Model extends CI_Model {
public function test_method($a, $b){
return $a * $b;
}
}
class Test_Controller extends CI_Controller {
public function __construct(){
$this->load->model('Test');
}
public function method1() {
$z = $this->Test->test_method(3,4);
}
}
As you can see, the model was loaded and all it's methods were available. In my opinion it's pretty straightforward.
Now, I've got the following in Laravel:
namespace App;
use Illuminate\Database\Eloquent\Model;
// Order Model
class Order extends Model
{
protected $fillable = ['user_id'];
public function orderItems()
{
return $this->hasMany(orderItem::class);
}
}
// orderItem model
namespace App;
use Illuminate\Database\Eloquent\Model;
class orderItem extends Model
{
protected $fillable = [
'order_id',
'item_id',
'type',
'quantity',
'price',
'subtotal'
];
public function order()
{
return $this->belongsTo(Order::class);
}
}
// Orders Controller
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Order;
use App\orderItem;
class OrdersController extends Controller
{
private $orderId;
public function store(Request $request)
{
// check if there's already a cart [order] for this user, if not create one
$this->orderId = Order::where('user_id', $request->json('user_id'))->get(['id']);
$item = [
'item_id' => $request->json('item_id'),
'type' => $request->json('type'),
'quantity' => $request->json('quantity'),
'price' => $request->json('price'),
];
if (!$this->orderId->count()){
$this->orderId = Order::insertGetId([
'user_id' => $request->json('user_id')
]);
}
$orderItem = new Order();
$orderItem->addOrderItem($item, $this->orderId);
}
}
Two questions I've got:
Is there a simpler or cleaner (not saying that creating a new obj is not clean) to access the Order model methods (such as done in Codeigniter)?
I've learnt a little bit about how to establish relationships between models in Laravel. I've got two other tables name Record and Artist respectively (a 1 to many relationship) and I can do the following:
$record = Record::findOrFail($id);
$record['artist_name'] = $record->artist->name;
but when I try to do the same with the Order and orderItem (also a 1 to many relationship) it doesn't work:
$cart = Order::where('user_id', $user->id)->get();
// Retrieve existing items in cart
$cart_items = $cart->orderItems();
Why is that?
As for question 1:
If the method has nothing to do with the actual instance of the model I would strongly recommend not putting it on the model. You could create a separate class that doesn't extend Model class as there is no need to.
If you really want to, you could create a static method though.
If it does depend on the model (database row), there's no way of not instantiating it as it will need to know which database row to work on.
As for question 2:
This part $cart_items = $cart->orderItems(); only returns a query builder (as you're calling it as a function and not a property). Which lets you chain other query builder methods off of it.
For example $cart_items = $cart->orderItems()->get(); will return the actual order items.
Or you could just call it as a property and get the same result:
$cart_items = $cart->orderItems;
While the above should work, it is generally suggested that you eager load the relationships (especially when you're pulling multiple rows of the parent model), which would look like this (the ->with() part will eager load them):
$cart = Order::where('user_id', $user->id)->with('orderItems')->get();
$cart_items = $cart->orderItems;

Laravel morph relationship

I have a question regarding saving polymorphic relationships in Laravel. This is the model i would like to create in laravel.
A shop has many products, and a product can be either an "item" an "event" or a "service".
I have the following tables:
shops
id
user_id
name
events
id
public
title
description
products
id
shop_id
productable_id
productable_type
This is how i set up the models:
class Shop extends Model{
public function products(){
return $this->hasMany('App\Product');
}
}
class Product extends Model{
public function productable(){
return $this->morphTo();
}
}
class Event extends Model{
public function product(){
return $this->morphOne('App\Product','productable');
}
}
I want to be able to do the following:
$shop = Shop::first()
$event = Event::create(['title'=>'Some event']);
$service = Service::create(['title' => 'Some service']);
$shop->products()->save($event);
$shop->products()->save($service);
But it doesn't work! When i try to save the relation i get:
Illuminate\Database\QueryException with message 'SQLSTATE[HY000]: General error: 1 no such column: shop_id (SQL: update "accendo_events" set "updated_at" = 2016-11-26 10:11:02, "shop_id" = 1 where "id" = 1)'
Anyone have an idea of where this is going wrong? I probably misunderstood something about this type of relationship...
First of all add a back relation to Shop from Product Model
class Shop extends Model
{
public function products()
{
return $this->hasMany('App\Product');
}
}
class Product extends Model
{
public function shop()
{
return $this->belongsTo('App\Shop');
}
public function productable()
{
return $this->morphTo();
}
}
class Event extends Model
{
public function product()
{
return $this->morphOne('App\Product', 'productable');
}
}
Now, I am not sure why you are trying to make an empty event and add it to all the products, but still if you want to do it for whatever use cases... please follow the below approach... :)
$shop = Shop::first(); //or $shop = Shop::find(1);
foreach($shop->products as $product) {
$event = Event::create([]);
$service = Service::create([]);
$product->productable()->saveMany([
$event, $service
]);
}
Let me know in the comments below if something doesn't work :)
-- Edit
First of all, please understand that you can not add an entry to productable_id or productable_type from a hasMany() relation. You need to make sure you are using a morph relation for such purposes.
Secondly, since you are trying to add products first and not events first, the insertion method is not working out for you. Please note that you must try to create an Event or Service first and then try to associate with a shop.
The simplest approach to doing it would be
$shop = Shop::first();
$event = Event::create(['title' => 'Some Event']);
$event->product()->create(['shop_id' => $shop->id]);
$service = Service::create(['title' => 'Some Service']);
$service->product()->create(['shop_id' => $shop->id]);
You can also, try to follow my first approach, but the one I just mentioned is definitely supposed to work... and actually that is how it is meant to be inserted/created.

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);
}
}

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