Laravel HasMany across multiple connections - php

I want to retrieve Offers related to Offer on a second table offer_related because I can't change the schema of the Offer table.
I have two databases on different connections, offers on one, and offer_related on another.
For the sake of argument, I'm going to name databases as follows for clarity in my examples with regards to which can change and which can't.
database containing offers henceforth known as immutable
database containing offer_related henceforth known as mutable
Example schema is as follows
connection1.mutable.offer_related
offer_id | related_offer_id
---------------------------
1 | 2
1 | 3
connection2.immutable.offers
id | name
---------------------------
1 | foo
2 | bar
3 | baz
I'm assuming it'd be a belongsToMany relationship, but I can't seem to get it right.
return $this->belongsToMany(Offer::class, 'immutable.offer', 'id');
// Syntax error or access violation: 1066 Not unique table/alias: 'offer'
I've also tried to manually build out a belongsToMany relationship with a custom query with no success.
I'd like to be able to call
Offer::find(1)->related; // Offer(2), Offer(3)

Change the relationship to:
return $this->belongsToMany(Offer::class, 'mutable.offer_related',
'offer_id', 'related_offer_id');
Your original query was trying to establish a relationship without using the relation table (offer_related). That is the problem.

i have this problem too, then solved with a trait:
<?php
namespace App\Traits\Model;
use Illuminate\Support\Str;
trait HasCrossDatabaseRelation {
public function getTable() {
if (! isset($this->table)) {
$this->table = str_replace(
'\\', '', Str::snake(Str::plural(class_basename($this)))
);
}
$configConnections = config('database.connections');
$databaseName = $configConnections[$this->getConnectionName()]['database'];
return $databaseName . "." . $this->table;
}
}
then use that trait and set the $connection property on each models. hope this help

Another solution is to create a trait:
trait HasCrossDbRelationships {
/**
* Define a one-to-many relationship with connection attribute
*
* #param string $connection
* #param string $related
* #param string $foreignKey
* #param string $localKey
* #return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function hasManyCrossDb($connection, $related, $foreignKey = null, $localKey = null)
{
$instance = $this->newRelatedInstance($related);
$instance->setConnection($connection);
$foreignKey = $foreignKey ?: $this->getForeignKey();
$localKey = $localKey ?: $this->getKeyName();
return $this->newHasMany(
$instance->newQuery(), $this, $instance->getTable().'.'.$foreignKey, $localKey
);
}
}

Related

Eloquent relationship where values are formatted differently

We've recently switched to a new permissions system on a project I am working on.
I have completed integration for this, eloquent relationships and all, when the requirements of the integration changed a little.
The permission system integrates with all of our systems across our infrastructure, and when it comes to referencing users from the Laravel project, the value in the permissions system is slightly different; in that it is prefixed with user-.
For example, a user with the username james in my users table is referenced as user-james in the permissions system table.
Is there any way to specify the value the eloquent relationship should look at?
I could just add a column to the users table to store the primary key of this user as it exists in the permissions table, but I wanted to see if there was a way to do this with eloquent.
If we consider relation is one - one we can do something like below:
First extend BelongsTo relation and change condition on where clause:
class CustomBelongsTo extends BelongsTo
{
/**
* #inheritDoc
*/
public function addConstraints()
{
if (static::$constraints) {
// For belongs to relationships, which are essentially the inverse of has one
// or has many relationships, we need to actually query on the primary key
// of the related models matching on the foreign key that's on a parent.
$table = $this->related->getTable();
$this->query->where($table.'.'.$this->otherKey, '=', 'user-'.$this->parent->{$this->foreignKey});
}
}
}
Then override belongsTo method on your model to use this custom relation.
class User extends Model {
protected $table = 'users';
public function permissions(){
return $this->belongsTo(Permission:class, 'username');
}
public function belongsTo($related, $foreignKey = null, $otherKey = null, $relation = null)
{
// If no relation name was given, we will use this debug backtrace to extract
// the calling method's name and use that as the relationship name as most
// of the time this will be what we desire to use for the relationships.
if (is_null($relation)) {
list($current, $caller) = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
$relation = $caller['function'];
}
// If no foreign key was supplied, we can use a backtrace to guess the proper
// foreign key name by using the name of the relationship function, which
// when combined with an "_id" should conventionally match the columns.
if (is_null($foreignKey)) {
$foreignKey = Str::snake($relation).'_id';
}
$instance = new $related;
// Once we have the foreign key names, we'll just create a new Eloquent query
// for the related models and returns the relationship instance which will
// actually be responsible for retrieving and hydrating every relations.
$query = $instance->newQuery();
$otherKey = $otherKey ?: $instance->getKeyName();
return new CustomBelongsTo($query, $this, $foreignKey, $otherKey, $relation);
}
}
I hope this help.

Laravel - Eloquent - Dynamically defined relationship

Is it possible to set a model's relationship dynamically? For example, I have model Page, and I want to add relationship banners() to it without actually changing its file? So does something like this exist:
Page::createRelationship('banners', function(){
$this->hasMany('banners');
});
Or something similar? As they are fetched using the magic methods anyway, perhaps I can add the relationship dynamically?
Thanks!
I've added a package for this i-rocky/eloquent-dynamic-relation
In case anyone still looking for a solution , here is one. If you think it's a bad idea, let me know.
trait HasDynamicRelation
{
/**
* Store the relations
*
* #var array
*/
private static $dynamic_relations = [];
/**
* Add a new relation
*
* #param $name
* #param $closure
*/
public static function addDynamicRelation($name, $closure)
{
static::$dynamic_relations[$name] = $closure;
}
/**
* Determine if a relation exists in dynamic relationships list
*
* #param $name
*
* #return bool
*/
public static function hasDynamicRelation($name)
{
return array_key_exists($name, static::$dynamic_relations);
}
/**
* If the key exists in relations then
* return call to relation or else
* return the call to the parent
*
* #param $name
*
* #return mixed
*/
public function __get($name)
{
if (static::hasDynamicRelation($name)) {
// check the cache first
if ($this->relationLoaded($name)) {
return $this->relations[$name];
}
// load the relationship
return $this->getRelationshipFromMethod($name);
}
return parent::__get($name);
}
/**
* If the method exists in relations then
* return the relation or else
* return the call to the parent
*
* #param $name
* #param $arguments
*
* #return mixed
*/
public function __call($name, $arguments)
{
if (static::hasDynamicRelation($name)) {
return call_user_func(static::$dynamic_relations[$name], $this);
}
return parent::__call($name, $arguments);
}
}
Add this trait in your model as following
class MyModel extends Model {
use HasDynamicRelation;
}
Now you can use the following method to add new relationships
MyModel::addDynamicRelation('some_relation', function(MyModel $model) {
return $model->hasMany(SomeRelatedModel::class);
});
As of laravel 7, dynamic relationship is officially supported. You can use the Model::resolveRelationUsing() method.
https://laravel.com/docs/7.x/eloquent-relationships#dynamic-relationships
you can use macro call for your dynamic relation like this:
you should write this code in your service provider boot method.
\Illuminate\Database\Eloquent\Builder::macro('yourRelation', function () {
return $this->getModel()->belongsTo('class');
});
You have to have something in mind, an Eloquent relationship is a model of a relational database relatioship (i.e. MySQL).
So, I came with two approaches.
The good
If you want to achieve a full-featured Eloquent relationship with indexes and foreing keys in the database, you probably want to alter the SQL tables dynamically.
For example, supossing you have all your models created and don't want to create them dynamically, you only have to alter the Page table, add a new field called "banner_id", index it and reference to "banner_id" field on Banner table.
Then you have to write down and support for the RDBMS you will work with.
After that, you may want to include support for migrations. If it's the case, you may store in the database these table alterations for further rollbacks.
Now, for the Eloquent support part, you may look at Eloquent Model Class.
See that, for each kind of relation, you have a subyacent model (all can be found here, which is in fact what you are returning in relatioship methods:
public function hasMany($related, $foreignKey = null, $localKey = null)
{
$foreignKey = $foreignKey ?: $this->getForeignKey();
$instance = new $related;
$localKey = $localKey ?: $this->getKeyName();
return new HasMany($instance->newQuery(), $this, $instance->getTable().'.'.$foreignKey, $localKey);
}
So you have to define a method in your model that accepts the type of relation and the model, creates a new HasMany (in case hasMany was the desired relationship) instance, and then returns it.
It's little bit complicated, and so you can use:
The easy
You can create a intermediate model (i.e. PageRelationship) that stores all the relationships between Page and other Models. A possible table schema could be:
+-------------+---------+------------------+-------------+
| relation_id | page_id | foreign_model_id | model_class |
+-------------+---------+------------------+-------------+
| 1 | 2 | 225 | Banner |
| 2 | 2 | 223 | Banner |
| 3 | 2 | 12 | Button |
+-------------+---------+------------------+-------------+
Then you can retrieve all dynamically relative models to a given Page. The problem here is that you don't actually have any real RDBMS relation between Models and Pages, so you may have to make multiple and heavy queries for loading related Models, and, what's worse, you have to manage yourself database consistency (i.e., deleting or updating the "225" Banner should also remove or update the row in page_relationship_table). Reverse relationships will be a headache too.
Conclusion
If the project is big, it depends on that, and you can't make a model that implements other models via inheritance or so, you should use the good approach. Otherwise, you should rethink you app design and then decide to choose or not second approach.
Just in case anyone is looking for a Laravel 8 answer:
Let's say I define my relationships in a single method of my model:
public function relationships()
{
return [
'user' => $this->belongsTo(User::class, 'user_id'),
];
}
Now, in my app service provider, I can use the resolveRelationUsing method. I've done this by iterating through the models folder and checking all models which contain the aforementioned method:
foreach ((new Filesystem)->allFiles(app_path('Models')) as $file) {
$namespace = 'App\\Models\\' . str_replace(['/', '.php'], ['\\', ''], $file->getRelativePathname());
$class = app($namespace);
if (method_exists($class, 'relationships')) {
foreach ($class->relationships() as $key => $relationship) {
$class->resolveRelationUsing($key, function () use ($class, $key) {
return $class->relationships()[$key];
});
}
}
}

Laravel - many-to-many where the many-to-many table is (part-) polymorph

I have a table called bonus. A user can get a bonus (it's like an reward) for certain actions. Well, the bonus can be assigned to many users and many users can get the same bonus. So it's a many to many relation between user and bonus.
This is no problem so far. But users can get the same bonus for different actions. So let's say there is a bonus for voting on a picture. Well, one user could vote on one picture and another one could vote on another picture which I'd like to save in the many-to-many table.
Furthermore there could be a bonus for writing a comment which is clearly another table than picture votes.
The problem here is that I would need to save the polymorphic type in the bonus table and the ID in the many-to-many table.
I think this should be the best way but how would I realize it with laravel? I think this is not a normal use case. But still I'd like to use it as other relations in laravel so that I could fetch a user and get his bonuses with the correct polymorphic relation.
Do you have any ideas?
You are probably going to have to develop your own relationship classes.
Ex:
MODEL
public function answers()
{
$instance = new Response();
$instance->setSid($this->sid);
return new QuestionAnswerRelation($instance->newQuery(),$this);
}
RELATIONSHIP
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\Relation;
use Pivotal\Survey\Models\Answer;
use Pivotal\Survey\Models\Collections\AnswerCollection;
use Pivotal\Survey\Models\QuestionInterface;
use Pivotal\Survey\Models\SurveyInterface;
class QuestionAnswerRelation extends Relation
{
/**
* Create a new relation instance.
*
* #param \Illuminate\Database\Eloquent\Builder $query
* #param \Illuminate\Database\Eloquent\Model $parent
* #return void
*/
public function __construct(Builder $query, QuestionInterface $parent)
{
$table = $query->getModel()->getTable();
$this->query = $query
->select(array(
\DB::raw($parent->sid.'X'.$parent->gid.'X'.$parent->qid . ' AS value'),
'id'
));
$this->query = $query;
$this->parent = $parent;
$this->related = $query->getModel();
$this->addConstraints();
}
public function addEagerConstraints(array $models)
{
parent::addEagerConstraints($models);
}
public function initRelation(array $models, $relation)
{
}
public function addConstraints()
{
}
public function match(array $models, Collection $results, $relation)
{
}
public function getResults()
{
$results = $this->query->get();
$answerCollection = new AnswerCollection();
foreach($results as $result)
{
$answer = new Answer($result->toArray());
$answer->question = $this->parent;
$answerCollection->add($answer);
}
return $answerCollection;
}
In this case we are using Lime Survey which creates a unique table (note the $instance->setSid() changes the table name) for each of its surveys and a unique column for each of its answer -> question values. ( note $parent->sid.'X'.$parent->gid.'X'.$parent->qid. 'AS value')
Where sid = survey_id, gid = group_id(I think) and qid = question_id
Its was quite irritating.
Note how I reference values from the parent to further develop the query.
You should be able to follow a similar route to achieve whatever your heart desires and still maintain the feasibility to use Eloquent.

Filtering column with json data using Eloquent ORM included with Laravel

I'm making a data filter and I need to filter a column with json saved data. The table structure is similar to this:
____________________________________
| id | name | tags |
|____|_________|___________________|
| 1 | example | ["3","4","5","6"] |
|____|_________|___________________|
Any idea how to filter this column by tags using Eloquent ORM included with Laravel?
Use the getter get<Column>Attribute:
class YourModel extends Eloquent {
public function getTagsAttribute($value)
{
return json_decode($value);
}
}
You could use mySql JSON_CONTAINS function since it is optimized on the DB level. Unfortunately Eloquent doesn't (?) provide wrappers for functions like this so you can fall back to plain SQL and create your models from the results.
This is an example that returns a Collection of Models:
class myModel extends Eloquent {
protected $casts = [
'json_column' => 'array', // this is JSON type in the DB
];
/**
* Returns a Collection of 'myModel' that have $search value
* stored in the 'json_column' array
*
* #param string $search
* #return \Illuminate\Database\Eloquent\Collection
*/
public static function findInJsonColumn($search){
$data = \DB::select("SELECT * FROM table_name WHERE JSON_CONTAINS(json_column, '[\"$search\"]')");
return self::hydrate($data);
}
}
class YourModel extends Eloquent {
public function getTagsAttribute($value)
{
parent::where($value)->get();
}
}

Laravel Removing Pivot data in many to many relationship

Not sure if I set this up correctly. In Laravel I'm creating two models with a many-to-may relationship
The models are Item and Tags. Each one contains a belongsTo to the other.
When I run a query like so:
Item::with('tags')->get();
It returns the collection of items, with each item containing a tags collection. However the each tag in the collection also contains pivot data which I don't need. Here it is in json format:
[{
"id":"49",
"slug":"test",
"order":"0","tags":[
{"id":"3","name":"Blah","pivot":{"item_id":"49","tag_id":"3"}},
{"id":"13","name":"Moo","pivot":{"item_id":"49","tag_id":"13"}}
]
}]
Is there anyway to prevent this data from getting at
you can just add the name of the field in the hidden part in your model like this:
protected $hidden = ['pivot'];
that's it , it works fine with me.
You have asked and you shall receive your answer. But first a few words to sum up the comment section. I personally don't know why you would want / need to do this. I understand if you want to hide it from the output but not selecting it from the DB really has no real benefit. Sure, less data will be transferred and the DB server has a tiny tiny bit less work to do, but you won't notice that in any way.
However it is possible. It's not very pretty though, since you have to override the belongsToMany class.
First, the new relation class:
class BelongsToManyPivotless extends BelongsToMany {
/**
* Hydrate the pivot table relationship on the models.
*
* #param array $models
* #return void
*/
protected function hydratePivotRelation(array $models)
{
// do nothing
}
/**
* Get the pivot columns for the relation.
*
* #return array
*/
protected function getAliasedPivotColumns()
{
return array();
}
}
As you can see this class is overriding two methods. hydratePivotRelation would normally create the pivot model and fill it with data. getAliasedPivotColumns would return an array of all columns to select from the pivot table.
Now we need to get this integrated into our model. I suggest you use a BaseModel class for this but it also works in the model directly.
class BaseModel extends Eloquent {
public function belongsToManyPivotless($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 BelongsToManyPivotless($query, $this, $table, $foreignKey, $otherKey, $relation);
}
}
I edited the comments out for brevity but otherwise the method is just like belongsToMany from Illuminate\Database\Eloquent\Model. Of course except the relation class that gets created. Here we use our own BelongsToManyPivotless.
And finally, this is how you use it:
class Item extends BaseModel {
public function tags(){
return $this->belongsToManyPivotless('Tag');
}
}
If you want to remove pivot data then you can use as protected $hidden = ['pivot']; #Amine_Dev suggested, so i have used it but it was not working for me,
but the problem really was that i was using it in wrong model so i want to give more detail in it that where to use it, so you guys don't struggle with the problem which i have struggled.
So if you are fetching the data as :
Item::with('tags')->get();
then you have to assign pivot to hidden array like below
But keep in mind that you have to define it in Tag model not in Item model
class Tag extends Model {
protected $hidden = ['pivot'];
}
Two possible ways to do this
1. using makeHidden method on resulting model
$items = Item::with('tags')->get();
return $items->makeHidden(['pivot_col1', 'pivot_col2']...)
2. using array_column function of PHP
$items = Item::with('tags')->get()->toArray();
return array_column($items, 'tags');

Categories