Laravel associate relationships by model instance dynamically - laravel

In my Laravel 9 project, My User Model is belongs to 3 models (Distributor, Agency and Advertiser) like:
public function distributor()
{
return $this->belongsTo(Distributor::class);
}
public function agency()
{
return $this->belongsTo(Agency::class);
}
public function advertiser()
{
return $this->belongsTo(Advertiser::class);
}
And I use this function to associate in my DistributorRepository(and other 2 model's repositories):
public function associateUser($id, $user_id)
{
$user = $this->user->find($user_id);
$distributor = $this->find($id);
$result = $user->distributor()->associate($distributor)->save();
return $result;
}
Now, I want to modify to associate them by user's id and relation model instance (Distributor, Agency and Advertiser) in my UserRepository (or UserService) dynamically, like:
public function associate($id, $modelInstance)
{
// Something happens here
}
Is that possible and how to do? Thanks!

If I understand right, then you can do it with:
public function associate($user_id, $parentModelInstance)
{
$user = User::find($user_id); //or how ever you get user in repository
$reflect = new ReflectionClass($parentModelInstance);
$relationName = Str::lower($reflect->getShortName()); //magic for getting relation (if class name is relation name)
return $user->{$relationName}()->associate($parentModelInstance)->save();
}
And you can use it like this:
$distributor = Distributor::find(1);
$user_id = 1;
$this->associate($user_id, $distributor)

Related

Get parent relation of an object dynamically in Laravel / Eloquent

I have three models
Product, Variant and Option.
class Product {
public $id;
public function variants(): HasMany
{
return $this->hasMany(Variant::class);
}
}
class Variant {
public $id;
public $product_id;
public function product(): BelongsTo
{
return $this->belongsTo(Product::class);
}
public function options(): HasMany
{
return $this->hasMany(Option::class);
}
}
class Option {
public $id;
public $variant_id;
public function variant(): BelongsTo
{
return $this->belongsTo(Variant::class);
}
}
I want to know if there is a way for an instance of Variant to get parent (Product) relationship and for Option
the parent (Variant) relationship with one line of code. Is there anything like the below?
$instance->parent();
I want to avoid writing
If (get_class($instance) === 'Variant' ) {
$instance->product();
} else if (get_class($instance) === 'Option' ) {
$instance->variant();
}
You can get the relation model easily. Like-
$variants = \App\Models\Variant::latest()->get();
foreach($variants as $variant)
{
$product_name = $variant->product->name; // this will be the product name that that related with this variant
}
It will be also work for single collection
$variant = \App\Models\Variant::find(1)>-first();
$variant->product;// this is product model instance that related to variant

Mutating model data

I want to manipulate data in my model how would I do that? I'm trying this but it doesn't work:
class Character extends Model
{
//Get players by level
public function scopeGetPlayersByLevel($query){
$output = $query->orderBy('level', 'desc')->get();
$output->classid = 100;
return $output;
}
//Get online status
public function account(){
$account = $this->belongsTo(Account::class, 'account_name', 'username');
$account->online = $account->online == 1 ? 'Online' : 'Offline';
return $account;
}
}
Define an accessor in the Account model:
public function getOnlineAttribute($value)
{
return $value === 1 ? 'Online' : 'Offline';
}
Try this:
a) Define a relationship for account model
b) Make an accessor (I called it getAccountStrAttribute, which means you call it like this: $acc->account_str).
c) Win
class Character extends Model
{
// Relationship defined
public function account() {
return $query->belongsTo(Account::class, 'account_name', 'username');
}
//Get online status
public function getAccountStrAttribute(){
return $this->account->online ? 'Online' : 'Offline';
}
// I can't figure out what you want this function to return/do :/
//Get players by level
public function scopeGetPlayersByLevel($query){
$output = $query->orderBy('level', 'desc')->get();
$output->classid = 100;
return $output;
}
}

belongsTo() function not working

I am trying to make a relation with my Character table and Account table but when I add the belongsTo() method I don't see the data of Account added to it.
So this is my Character model:
class Character extends Model
{
//Get players by level
public function scopeGetPlayersByLevel($query){
return $query->orderBy('level', 'desc')->get();
}
//Get players by zulie
public function scopeGetPlayersByZulie($query){
return $query->orderBy('zuly', 'desc')->get();
}
//Get online status
public function account(){
return $this->belongsTo(Account::class, 'username', 'account_name');
}
}
Account model:
class Account extends Model
{
//Get online players
public function scopeGetOnlinePlayers($query){
return $query->where('online', 1);
}
//Get online staff
public function scopeGetOnlineStaff($query){
return $query->where('online', 1)->where('accesslevel', 350);
}
public function characters(){
return $this->hasMany(Character::class, 'account_name', 'username');
}
}
And I add the variable in my controller like this:
use App\Character;
class RankingController extends Controller
{
function index(){
$players = Character::find(100);
return $players;
}
}
Table structure:
accounts:
- username
characters:
- account_name //foreign key
The result of $players is only the character data. Why doesn't the account data add to it?
If I understand what you are trying to do is you are trying to get a player and all their characters.
try
function index(){
$player = Account::find(1);
return $player->characters;
}
Find the account of a character
$caracter = Character::find(1);
return $caracter->account;
Try this:
//Get online status
public function account(){
return $this->belongsTo(Account::class, 'account_name', 'username');
}

Eloquent relationships for not-existing model class

I would like to have in my applications many models/modules but some of them would be removed for some clients.
Now I have such relation:
public function people()
{
return $this->hasMany('People', 'model_id');
}
and when I run $model = Model::with('people')->get(); it is working fine
But what if the People model doesn't exist?
At the moment I'm getting:
1/1 ErrorException in ClassLoader.php line 386: include(...): failed
to open stream: No such file or directory
I tried with
public function people()
{
try {
return $this->hasMany('People', 'model_id');
}
catch (FatalErrorException $e) {
return null;
}
}
or with:
public function people()
{
return null; // here I could add checking if there is a Model class and if not return null
}
but when using such method $model = Model::with('people')->get(); doesn't work.
I will have a dozens of relations and I cannot have list of them to use in with. The best method for that would be using some empty relation (returning null) just to make Eloquent not to do anything but in this case Eloquent still tries to make it work and I will get:
Whoops, looks like something went wrong.
1/1 FatalErrorException in Builder.php line 430: Call to a member function
addEagerConstraints() on null
Is there any simple solution for that?
The only solution I could come up with is creating your own Eloquent\Builder class.
I've called it MyBuilder. Let's first make sure it gets actually used. In your model (preferably a Base Model) add this newEloquentBuilder method:
public function newEloquentBuilder($query)
{
return new MyBuilder($query);
}
In the custom Builder class we will override the loadRelation method and add an if null check right before addEagerConstraints is called on the relation (or in your case on null)
class MyBuilder extends \Illuminate\Database\Eloquent\Builder {
protected function loadRelation(array $models, $name, Closure $constraints)
{
$relation = $this->getRelation($name);
if($relation == null){
return $models;
}
$relation->addEagerConstraints($models);
call_user_func($constraints, $relation);
$models = $relation->initRelation($models, $name);
$results = $relation->getEager();
return $relation->match($models, $results, $name);
}
}
The rest of the function is basically the identical code from the original builder (Illuminate\Database\Eloquent\Builder)
Now simply add something like this in your relation function and it should all work:
public function people()
{
if(!class_exist('People')){
return null;
}
return $this->hasMany('People', 'model_id');
}
Update: Use it like a relationship
If you want to use it like you can with a relationship it gets a bit more tricky.
You have to override the getRelationshipFromMethod function in Eloquent\Model. So let's create a Base Model (Your model obviously needs to extend it then...)
class BaseModel extends \Illuminate\Database\Eloquent\Model {
protected function getRelationshipFromMethod($key, $camelKey)
{
$relations = $this->$camelKey();
if ( $relations instanceof \Illuminate\Database\Eloquent\Collection){
// "fake" relationship
return $this->relations[$key] = $relations;
}
if ( ! $relations instanceof Relation)
{
throw new LogicException('Relationship method must return an object of type '
. 'Illuminate\Database\Eloquent\Relations\Relation');
}
return $this->relations[$key] = $relations->getResults();
}
}
Now we need to modify the relation to return an empty collection
public function people()
{
if(!class_exist('People')){
return new \Illuminate\Database\Eloquent\Collection();
}
return $this->hasMany('People', 'model_id');
}
And change the loadRelation function in MyBuilder to check for the type collection instead of null
protected function loadRelation(array $models, $name, Closure $constraints)
{
$relation = $this->getRelation($name);
if($relation instanceof \Illuminate\Database\Eloquent\Collection){
return $models;
}
// ...
}

Saving related records in laravel

I have users, and users belong to a dealership.
Upon user registration, I'm trying to save a new user, and a new dealership.
User database has a dealership_id column, which I want to be populated with the ID of the newly created dealership.
This is my current code in the UserController store method.
public function store()
{
$user = new User();
$user->email = Input::get('email');
$user->password = Input::get('password');
$dealership = new Dealership();
$dealership->name = Input::get('dealership_name');
$user->push();
return "User Saved";
}
Trying to use $user->push(); User data gets updated, but dealership is not created or updated.
Eloquent's push() saves the model and its relationships, but first you have to tell what you want to be involved in the relationsship.
Since your user-model/table holds the id of the dealership, I assume that a user can belong to only one dealership, so the relationship should look like this:
User Model:
public function dealership()
{
return $this->belongsTo('Dealership');
}
Dealership Model:
public function users()
{
return $this->hasMany('User');
}
To save a User from the Dealership perspective, you do this:
$dealership->users()->save($user);
To associate a dealership with a user, you do this:
$user->dealership()->associate($dealership);
$user->save();
Please check this answer to see the difference of push() and save()
You will need to define correctly your models relationships as per documentation
If this is done correctly, it should work .
This is what push() does :
/**
* Save the model and all of its relationships.
*
* #return bool
*/
public function push()
{
if ( ! $this->save()) return false;
// To sync all of the relationships to the database, we will simply spin through
// the relationships and save each model via this "push" method, which allows
// us to recurse into all of these nested relations for the model instance.
foreach ($this->relations as $models)
{
foreach (Collection::make($models) as $model)
{
if ( ! $model->push()) return false;
}
}
return true;
}
In your case, you have a one (dealership) belongs to many (users)
In your Users model :
class Users extends Eloquent {
public function dealership()
{
return $this->belongsTo('Dealership');
}
}
In the example above, Eloquent will look for a dealership_id column on the users table.
In your Dealership Model :
class Dealership extends Eloquent {
public function users()
{
return $this->hasMany('User');
}
}
In your store function :
public function store()
{
$user = new User();
$user->email = Input::get('email');
$user->password = Input::get('password');
$user->dealership = new Dealership();
$user->dealership->name = Input::get('dealership_name');
$user->push();
return "User Saved";
}
Learn here more about eloquent relationships
Also please take a look at my answer here
By using push on the User model, Laravel is basically recursively calling save on all the related models (which, in this case, is none, since you haven't associated any other models to it yet).
Therefore, in order to accomplish what you're trying to do, you can do first create the user then associate the dealership with it by doing the following:
$user = new User();
$user->email = Input::get('email');
$user->password = Input::get('password');
$user->save();
$dealership = new Dealership();
$dealership->name = Input::get('dealership_name');
$user->dealerships()->save($dealership);
return "User Saved";
However, prior to doing this, you must ensure your User and Dealership models have their relationships set up correctly:
User Model:
public function dealership()
{
return $this->belongsTo('Dealership');
}
Dealership Model:
public function users()
{
return $this->hasMany('User');
}
This is how I manage to do it.
In your controller: (Laravel 5)
use Auth;
$dealership->user = Auth::user()->ref_user->staff_id;

Resources