Laravel hasManyThrough relationship with non-default local key - php

My schema is as follows:
Clients (hasMany Accounts)
id
name
Accounts (hasMany Holdings, belongsTo Clients)
id (int)
account_id (string, unique key)
name
Holdings (belongsTo Accounts)
id
account_id (string)
value
holding_date... etc
So, Client hasMany Accounts hasMany Holdings. The caveat being that the local key for accounts is account_id, not just id as is expected. This is because there is a requirement for the accounts to have a string identifier. In the holdings table the foreign key is also account_id.
I have defined my relationships like so:
// Client.php
public function accounts()
{
return $this->hasMany('Account');
}
// Account.php
public function client()
{
return $this->belongsTo('Client');
}
public function holdings()
{
return $this->hasMany('Holding');
}
// Holding.php
public function account()
{
return $this->belongsTo('Account', 'account_id', 'account_id');
}
If I wanted to query all the holdings for a given client ID how would I do this? If I do something like
Client::find($id)->accounts->holdings;
I get this error:
Undefined property: Illuminate\Database\Eloquent\Relations\HasMany::$holdings
I also tried using the hasManyThrough relationship (having added the relationship to my model) but there seems to only be a way to define the foreign key, not the local key for the accounts. Any suggestions?

Assuming you have client_id on accounts table,
do this:
// Account model
public function holdings()
{
return $this->hasMany('Holding', 'account_id', 'account_id');
}
// then
$client = Client::with('accounts.holdings')->find($id);
$client->accounts // collection
->first() // or process the collecction in the loop
->holdings; // holdlings collection
HasManyThrough will work only if Account model has (or will have for that purpose) $primaryKey set to account_id instead of default id
Since account_id is not primary key of the Account model, you can't use hasManyThrough. So I suggest you do this:
$accountIds = $client->accounts()->lists('account_id');
// if it was many-to-many you would need select clause as well:
// $accountIds = $client->accounts()->select('accounts.account_id')->lists('account_id');
$holdings = Holding::whereIn('account_id', $accountIds)->get();
This way you get the Collection just like you wanted, donwside is 1 more query needed in comparison to eager loading.

You need to change your relation in Account model
// Account.php
public function client()
{
return $this->belongsTo('Client','account_id');
}
But, it more appropriate to change column name to client_id in Accounts table

I think you can use load method to get the corresponding resulting query for each accounts. Something like:
Client::find($id)->load('accounts.holdings');
This means that client_id is present in accounts and holdings has account_id as well.
PS: I am not super sure how this would work in this context. But I hope this can lead you to find the way to do it.

You'd have to override Eloquent a little bit. I just ran into something very similar with a BelongsToMany relationship. I was trying to perform a many-to-many query where the relevant local-key was not the primary key. So I extended Eloquent's BelongsToMany a little bit. Start by building an override class for the BelongsToMany relationship class:
namespace App\Overrides\Relations;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany as BaseBelongsToMany;
class BelongsToMany extends BaseBelongsToMany
{
protected $localKey;
/**
* #var array
*/
protected $customConstraints = [];
/**
* BelongsToMany constructor.
* #param Builder $query
* #param Model $parent
* #param string $table
* #param string $foreignKey
* #param string $otherKey
* #param string $relationName
* #param string $localKey
*/
public function __construct(
Builder $query,
Model $parent,
$table,
$foreignKey,
$otherKey,
$relationName = null,
$localKey = null
) {
//The local-key binding, assumed by Eloquent to be the primary key of the model, will have already been set
if ($localKey) { //If it's intended to be overridden, that value in the Query/Builder object needs updating
$this->localKey = $localKey;
$this->setLocalKey($query, $parent, $table, $foreignKey, $localKey);
}
parent::__construct($query, $parent, $table, $foreignKey, $otherKey, $relationName);
}
/**
* If a custom local-key field is defined, don't automatically assume the pivot table's foreign relationship is
* joined to the model's primary key. This method is necessary for lazy-loading.
*
* #param Builder $query
* #param Model $parent
* #param string $table
* #param string $foreignKey
* #param string $localKey
*/
public function setLocalKey(Builder $query, Model $parent, $table, $foreignKey, $localKey)
{
$qualifiedForeignKey = "$table.$foreignKey";
$bindingIndex = null;
//Search for the 'where' value currently linking the pivot table's foreign key to the model's primary key value
$query->getQuery()->wheres = collect($query->getQuery()->wheres)->map(function ($where, $index) use (
$qualifiedForeignKey,
$parent,
&$bindingIndex
) {
//Update the key value, and note the index so the corresponding binding can also be updated
if (array_get($where, 'column', '') == $qualifiedForeignKey) {
$where['value'] = $this->getKey($parent);
$bindingIndex = $index;
}
return $where;
})->toArray();
//If a binding index was discovered, updated it to reflect the value of the custom-defined local key
if (!is_null($bindingIndex)) {
$bindgings = $query->getQuery()->getBindings();
$bindgings[$bindingIndex] = $this->getKey($parent);
$query->getQuery()->setBindings($bindgings);
}
}
/**
* Get all of the primary keys for an array of models.
* Overridden so that the call to $value->getKey() is replaced with $this->getKey()
*
* #param array $models
* #param string $key
* #return array
*/
protected function getKeys(array $models, $key = null)
{
if ($key) {
return parent::getKeys($models, $key);
}
return array_unique(array_values(array_map(function ($value) use ($key) {
return $this->getKey($value);
}, $models)));
}
/**
* If a custom local-key field is defined, don't automatically assume the pivot table's foreign relationship is
* joined to the model's primary key. This method is necessary for eager-loading.
*
* #param Model $model
* #return mixed
*/
protected function getKey(Model $model)
{
return $this->localKey ? $model->getAttribute($this->localKey) : $model->getKey();
}
/**
* Set the where clause for the relation query.
* Overridden so that the call to $this->parent->getKey() is replaced with $this->getKey()
* This method is not necessary if this class is accessed through the typical flow of a Model::belongsToMany() call.
* It is necessary if it's instantiated directly.
*
* #return $this
*/
protected function setWhere()
{
$foreign = $this->getForeignKey();
$this->query->where($foreign, '=', $this->getKey($this->parent));
return $this;
}
}
Next, you'll need to make the Model class actually use it:
namespace App\Overrides\Traits;
use App\Overrides\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\Relation;
/**
* Intended for use inside classes that extend Illuminate\Database\Eloquent\Model
*
* Class RelationConditions
* #package App\Overrides\Traits
*/
trait CustomConstraints
{
/**
* Intercept the Eloquent Model method and return a custom relation object instead
*
* {#inheritdoc}
*/
public function belongsToMany($related, $table = null, $foreignKey = null, $otherKey = null, $relation = null, $localKey = null)
{
//Avoid having to reproduce parent logic here by asking the returned object for its original parameter values
$base = parent::belongsToMany($related, $table, $foreignKey, $otherKey, $relation);
//The base action will have already applied the appropriate constraints, so don't re-add them here
return Relation::noConstraints(function () use ($base, $localKey) {
//These methods do the same thing, but got renamed
$foreignKeyName = version_compare(app()->version(), '5.3', '>=')
? $base->getQualifiedForeignKeyName()
: $base->getForeignKey();
$relatedKeyName = version_compare(app()->version(), '5.3', '>=')
? $base->getQualifiedRelatedKeyName()
: $base->getOtherKey();
return new BelongsToMany(
$base->getQuery(),
$base->getParent(),
$base->getTable(),
last(explode('.', $foreignKeyName)),
last(explode('.', $relatedKeyName)),
$base->getRelationName(),
$localKey
);
});
}
}
Use this trait inside your model class, and you now have the ability to add a 6th argument that specifies what local-key to use, rather than automatically assume the primary one.

Related

relatableQuery() for two resource fields on same model in Laravel Nova

I have a relationship between work 'days' and projects of different types. So my 'days' record has a reference to my 'projects' table twice because I have two different types of projects called 'Series' and 'Event'.
In my 'days' resource I've created two fields as such:
BelongsTo::make('Series','series',Project::class)->sortable()->nullable(),
BelongsTo::make('Event','event',Project::class)->sortable()->nullable(),
What I'm trying to do is filter the projects by their types so I've created this:
public static function relatableProjects(NovaRequest $request, $query){
return $query->where('type', 'Series');
}
I've tried making relatableSeries and relatableEvents but they don't work. How can I make this connect to the fields correctly without having to create two separate tables for 'series' and 'events'.
The relatableQuery above winds up filtering both resource fields.
Because relatableQuery() is referencing a relatableModel() (so relatableProjects() references the Project model) I was able to create another model solely for the purpose of helping with this.
I created an Event model which references the same projects table and then was able to create a relatableEvents() method to use the where() filter query.
Note: I did have to also create an Event resource which references the Event model since this is how Nova works but was able to hide it from being accessed which you can find more information about here
See revised BelongsTo fields and new model below:
Day resource
/**
* Build a "relatable" query for the given resource.
*
* This query determines which instances of the model may be attached to other resources.
*
* #param \Laravel\Nova\Http\Requests\NovaRequest $request
* #param \Illuminate\Database\Eloquent\Builder $query
* #return \Illuminate\Database\Eloquent\Builder
*/
public static function relatableProjects(NovaRequest $request, $query){
return $query->where('type', 'Series');
}
/**
* Build a "relatable" query for the given resource.
*
* This query determines which instances of the model may be attached to other resources.
*
* #param \Laravel\Nova\Http\Requests\NovaRequest $request
* #param \Illuminate\Database\Eloquent\Builder $query
* #return \Illuminate\Database\Eloquent\Builder
*/
public static function relatableEvents(NovaRequest $request, $query){
return $query->where('type', 'Event');
}
/**
* Get the fields displayed by the resource.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function fields(Request $request)
{
return [
ID::make()->hideFromIndex()->hideFromDetail()->hideWhenUpdating(),
BelongsTo::make('User','user',User::class)->sortable(),
BelongsTo::make('Budget','budget',Budget::class)->sortable()->nullable(),
BelongsTo::make('Series','series',Project::class)->sortable()->nullable(),
BelongsTo::make('Event','event',Event::class)->sortable()->nullable(),
DateTime::make('Last Updated','updated_at')->hideFromIndex()->readOnly(),
new Panel('Schedule',$this->schedule()),
new Panel('Time Entry',$this->timeEntries()),
];
}
Event model
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Event extends Model
{
/**
* The table associated with the model.
*
* #var string
*/
protected $table = 'projects';
protected $casts = [
'starts_on' => 'date',
'ends_on' => 'date',
];
public function event(){
return $this->belongsTo('App\Event');
}
public function project(){
return $this->belongsTo('App\Project');
}
}
i know it is an old question but i was facing same problem with laravel nova belongsto field, in some resource i have a belongsto that relates to users but these should be of 'supervisor' role in another resource i hace a belongsto field relating to users but these should be of role 'guard', as laravel nova belongsto field just takes all users in both selects all users appeared and it seems nova belongsto field doe not have a way, or at least i did not find it to scope the query, so what i did was creating a php class named BelongstoScoped this class extends laravel nova field BelongsTo so i overwrote the method responsible of creating the query
<?php
namespace App\Nova\Customized;
use Laravel\Nova\Query\Builder;
use Laravel\Nova\Fields\BelongsTo;
use Laravel\Nova\Http\Requests\NovaRequest;
class BelongsToScoped extends BelongsTo
{
private $modelScopes = [];
//original function in laravel belongsto field
public function buildAssociatableQuery(NovaRequest $request, $withTrashed = false)
{
$model = forward_static_call(
[$resourceClass = $this->resourceClass, 'newModel']
);
$query = new Builder($resourceClass);
//here i chaned this:
/*
$query->search(
$request, $model->newQuery(), $request->search,
[], [], ''
);
*/
//To this:
/*
$query->search(
$request, $this->addScopesToQuery($model->newQuery()), $request->search,
[], [], ''
);
*/
//The method search receives a query builder as second parameter, i just passed the result of custom function
//addScopesToQuery as second parameter, thi method returns the same query but with the model scopes passed
$request->first === 'true'
? $query->whereKey($model->newQueryWithoutScopes(), $request->current)
: $query->search(
$request, $this->addScopesToQuery($model->newQuery()), $request->search,
[], [], ''
);
return $query->tap(function ($query) use ($request, $model) {
forward_static_call($this->associatableQueryCallable($request, $model), $request, $query, $this);
});
}
//this method reads the property $modelScopes and adds them to the query
private function addScopesToQuery($query){
foreach($this->modelScopes as $scope){
$query->$scope();
}
return $query;
}
// this method should be chained tho the field
//example: BelongsToScoped::make('Supervisores', 'supervisor', 'App\Nova\Users')->scopes(['supervisor', 'active'])
public function scopes(Array $modelScopes){
$this->modelScopes = $modelScopes;
return $this;
}
}
?>
In my users model i have the scopes for supervisors and guard roles like this:
public function scopeActive($query)
{
return $query->where('state', 1);
}
public function scopeSupervisor($query)
{
return $query->role('supervisor');
}
public function scopeSuperadmin($query)
{
return $query->role('superadmin');
}
public function scopeGuarda($query)
{
return $query->role('guarda');
}
So in the laravel nova resource i just included the use of this class
*remember the namespace depends on how you name your file, in my case i created the folder Customized and included the file there:
use App\Nova\Customized\BelongsToScoped;
In the fields in nova resource i used like this:
BelongsToScoped::make('Supervisor', 'supervisorUser', 'App\Nova\Users\User')
->scopes(['supervisor', 'active'])
->searchable()
So that way i could call the belongsto field in the nova resources which filter users depending on modle scopes.
I hope this helps someone, sorry if my English is not that good.

Laravel Eloquent Case Sensitive Relationships

Hi SO I'm having real issues with some Laravel Eloquent relationships which I can only guess are being caused by a case-sensitive relation and I'm hoping somebody here can help!
Here are the models that I'm having the issues with:
class DeliveryManifestLines extends Eloquent
{
protected $table = 'manifests';
public function sapDelivery()
{
return $this->hasOne('Delivery', 'DocNum', 'sap_delivery');
}
}
class Delivery extends Eloquent
{
protected $connection = 'sap';
protected $table = 'ODLN';
protected $primaryKey = 'DocNum';
public function deliveryManifest() {
return $this->belongsTo('DeliveryManifest', 'DocNum', 'sap_delivery');
}
public function address()
{
return $this->hasOne('Address', 'Address', 'ShipToCode')->where('CardCode', $this->CardCode)->where('AdresType', 'S');
}
public function geolocation()
{
return $this->hasOne('GeoLocation', 'Address', 'ShipToCode')->where('CardCode', $this->CardCode)->where('AdresType', 'S')->where('Lat', '>', 0)->where('Lng', '>', 0);
}
}
class Address extends Eloquent
{
protected $connection = 'sap';
protected $table = 'CRD1';
protected $primaryKey = 'Address';
public function delivery() {
return $this->belongsTo('Delivery', 'Address', 'ShipToCode');
}
}
Here's the code in my controller that is supposed to fetch some of the above models from the DB.
$deliveries = DeliveryManifestLines::with('sapDelivery')->where('manifest_date', $date))->get();
foreach ($deliveries as $delivery) {
$delivery->sapDelivery->load('address');
}
I'm using the "->load('address)" line as no matter what I tried I could not get eager loading to work with "sapDelivery.address"
In 99% of cases the address is loaded successfully from the DB but I have come across one case in which I am experiencing an issue that I can only think is being caused by case-sensitivity.
Using Laravel DebugBar I can see that my application is executing the following query:
SELECT * FROM [CRD1] WHERE [CardCode] = 'P437' AND [AdresType] = 'S' AND [CRD1].[Address] IN ('The Pizza Factory (topping)')
When I dump the contents of $delivery->sapDelivery in this occurrence the address relation is NULL, however, when I paste the SQL statement into my DB console and execute it manually I get the expected row returned.
The only difference I can see between this one address and the thousands of others that are working is that there is a case difference between the Address fields:
In the CRD1 table the Address field for the effected/expected row is "The Pizza Factory (Topping)" but the eloquent relationship is using AND [CRD1].[Address] IN ('The Pizza Factory (topping)') to try and find it I'm aware that SQL is case-insensitive be default but I can't think of any other reason why this one row is behaving differently to the others.
Does anybody have any other ideas as to what could be causing this issue and suggest any possible solutions or confirm either way my theory of case sensitivity being the culprit.
Many thanks!
So After giving this problem little thought over the past few months I revisited the issue today and found some very useful code on laravel.io by somebody experiencing the same issue I found myself with.
I've built on MattApril's solution to provide the least hacky way I can think of to provide a way to offer case insensitive relationships in laravel.
To achieve this you need to add a few new classes which utilise the strtolower() function to create lower case keys which allows the isset() function used in the relationships to find differently cased but matching keys:
ModelCI.php (app\Models\Eloquent\ModelCI.php)
<?php
namespace App\Models\Eloquent;
use App\Models\Eloquent\Relations\BelongsToCI;
use App\Models\Eloquent\Relations\HasManyCI;
use App\Models\Eloquent\Relations\HasOneCI;
use Illuminate\Database\Eloquent\Model;
abstract class ModelCI extends Model
{
/**
* Define a one-to-many relationship.
*
* #param string $related
* #param string $foreignKey
* #param string $localKey
*
* #return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function hasManyCI($related, $foreignKey = null, $localKey = null)
{
$foreignKey = $foreignKey ?: $this->getForeignKey();
$instance = new $related();
$localKey = $localKey ?: $this->getKeyName();
return new HasManyCI($instance->newQuery(), $this, $instance->getTable().'.'.$foreignKey, $localKey);
}
/**
* Define a one-to-one relationship.
*
* #param string $related
* #param string $foreignKey
* #param string $localKey
* #return \Illuminate\Database\Eloquent\Relations\HasOne
*/
public function hasOneCI($related, $foreignKey = null, $localKey = null)
{
$foreignKey = $foreignKey ?: $this->getForeignKey();
$instance = new $related;
$localKey = $localKey ?: $this->getKeyName();
return new HasOneCI($instance->newQuery(), $this, $instance->getTable().'.'.$foreignKey, $localKey);
}
/**
* Define an inverse one-to-one or many relationship.
*
* #param string $related
* #param string $foreignKey
* #param string $otherKey
* #param string $relation
* #return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function belongsToCI($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(, $caller) = debug_backtrace(false, 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 = snake_case($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 BelongsToCI($query, $this, $foreignKey, $otherKey, $relation);
}
}
BelongsToCI.php (app\Models\Eloquent\Relations\BelongsToCI.php)
<?php namespace App\Models\Eloquent\Relations;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\Expression;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class BelongsToCI extends BelongsTo {
/**
* Match the eagerly loaded results to their parents.
*
* #param array $models
* #param \Illuminate\Database\Eloquent\Collection $results
* #param string $relation
* #return array
*/
public function match(array $models, Collection $results, $relation)
{
$foreign = $this->foreignKey;
$other = $this->otherKey;
// First we will get to build a dictionary of the child models by their primary
// key of the relationship, then we can easily match the children back onto
// the parents using that dictionary and the primary key of the children.
$dictionary = array();
foreach ($results as $result)
{
$dictionary[strtolower($result->getAttribute($other))] = $result;
}
// Once we have the dictionary constructed, we can loop through all the parents
// and match back onto their children using these keys of the dictionary and
// the primary key of the children to map them onto the correct instances.
foreach ($models as $model)
{
if (isset($dictionary[strtolower($model->$foreign)]))
{
$model->setRelation($relation, $dictionary[strtolower($model->$foreign)]);
}
}
return $models;
}
}
HasManyCI.php (app\Models\Eloquent\Relations\HasManyCI.php)
<?php namespace App\Models\Eloquent\Relations;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\HasMany;
class HasManyCI extends HasMany {
/**
* Build model dictionary keyed by the relation's foreign key.
*
* #param \Illuminate\Database\Eloquent\Collection $results
* #return array
*/
protected function buildDictionary(Collection $results)
{
$dictionary = array();
$foreign = $this->getPlainForeignKey();
// First we will create a dictionary of models keyed by the foreign key of the
// relationship as this will allow us to quickly access all of the related
// models without having to do nested looping which will be quite slow.
foreach ($results as $result)
{
$dictionary[strtolower($result->{$foreign})][] = $result;
}
return $dictionary;
}
/**
* Match the eagerly loaded results to their many parents.
*
* #param array $models
* #param \Illuminate\Database\Eloquent\Collection $results
* #param string $relation
* #param string $type
* #return array
*/
protected function matchOneOrMany(array $models, Collection $results, $relation, $type)
{
$dictionary = $this->buildDictionary($results);
// Once we have the dictionary we can simply spin through the parent models to
// link them up with their children using the keyed dictionary to make the
// matching very convenient and easy work. Then we'll just return them.
foreach ($models as $model)
{
$key = strtolower( $model->getAttribute($this->localKey) );
if (isset($dictionary[$key]))
{
$value = $this->getRelationValue($dictionary, $key, $type);
$model->setRelation($relation, $value);
}
}
return $models;
}
}
HasOneCI.php (app\Models\Eloquent\Relations\HasOneCI.php)
<?php namespace App\Models\Eloquent\Relations;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\HasOne;
class HasOneCI extends HasOne {
/**
* Match the eagerly loaded results to their many parents.
*
* #param array $models
* #param \Illuminate\Database\Eloquent\Collection $results
* #param string $relation
* #param string $type
* #return array
*/
protected function matchOneOrMany(array $models, Collection $results, $relation, $type)
{
$dictionary = $this->buildDictionary($results);
// Once we have the dictionary we can simply spin through the parent models to
// link them up with their children using the keyed dictionary to make the
// matching very convenient and easy work. Then we'll just return them.
foreach ($models as $model)
{
$key = strtolower($model->getAttribute($this->localKey));
if (isset($dictionary[$key]))
{
$value = $this->getRelationValue($dictionary, $key, $type);
$model->setRelation($relation, $value);
}
}
return $models;
}
/**
* Build model dictionary keyed by the relation's foreign key.
*
* #param \Illuminate\Database\Eloquent\Collection $results
* #return array
*/
protected function buildDictionary(Collection $results)
{
$dictionary = array();
$foreign = strtolower($this->getPlainForeignKey());
// First we will create a dictionary of models keyed by the foreign key of the
// relationship as this will allow us to quickly access all of the related
// models without having to do nested looping which will be quite slow.
foreach ($results as $result)
{
$dictionary[$result->{$foreign}][] = $result;
}
return $dictionary;
}
}
To utilise the new classes you must define a relationship as so:
$this->belongsToCI('Model');
or
$this->hasManyCI('Model');
or
$this->hasOneCI('Model');
Eloquent uses internal associative arrays to map and link the related records to their parents and in cases with string foreign keys and difference in uppercase vs lowercase, some of the related records will not be mapped.
I recently came across the same problem and decided to wrap a solution in a composer package.
https://github.com/TishoTM/eloquent-ci-relations
After installing the composer package, you can simply use the trait inside the eloquent models without changing anything else on the relation methods within the model.
use \TishoTM\Eloquent\Concerns\HasCiRelationships;

How to change table of model dynamically in laravel 4.2?

It is known that after creating a new model, there is a function to change the table used by the model.
For example,
$example_model = new ExampleModel;
$example_model->setTable('myTable_'.$some_other_variable);
However, if I am finding record from table, is there a way to choose the table before querying the database?
i.e. something like this
$example_model = ExampleModel::setTable('myTable_'.$some_other_variable)->where('myColumn', $variable_to_be_compared)->get();
(Noticed that the following line is not correct. I will says setTable is not a static method)
I have some custom function in my model, so I would prefer not to use DB::table('myTable_'.$some_other_variable).
The problem with the upstream setTable() method is that it returns nothing (void) so even you manage to call it you won't be able to chain it with other methods unless you override it.
// File: ExampleModel.php
/**
* Set the table associated with the model.
*
* #param string $table
* #return self
*/
public function setTable($table)
{
$this->table = $table;
return $this;
}
Then you can do something like this
$example_model = with(new ExampleModel)->setTable('myTable_'.$some_other_variable)->where('myColumn', $variable_to_be_compared)->get();
But since that solution involves writing the method, you could write instead a new static method for the task so you don't need to use helpers
/**
* Change the table associated with the model.
*
* #param string $table
* #param array $attributes
* #return self
*/
public static function changeTable($table, $attributes = [])
{
$instance = new static($attributes);
$instance->setTable($table);
return $instance;
}
Which can be use
$example_model = ExampleModel::changeTable('myTable_'.$some_other_variable)->where('myColumn', $variable_to_be_compared)->get();

Eloquent Relationships

Long story short: I'm building a "privacy" page where uses can chose what shows up and what does not show up on their profiles.
I am considering having a 1:m table user:privacy and just have entries for the keys they want private. If they don't exist they are public. Hope this makes sense.
Table would be user_privacy and will have 3 columns: id, user_id, privacy_key (string, i.e. email/phone/cell/etc)
Is there a way to simple query by the keys i will define that i can run to determine if the user has a key or not or do i have to go extra lengths to add a function to the user model to do this (trying to avoid, love the magic-ness of eloquent)
Basically i want to have a condition that sounds like "if ($user->privacy->email or $user->privacy->phone)"
Thanks and hope i was clear enough, lol
You could add a function to your user model:
public function isPrivate($attribute){
$privacyAttribute = $this->privacy->first(function($model) use ($attribute){
return $model->key == $attribute; // key being the column in the privacy model
});
return !is_null($privacyAttribute);
}
And then do your if statement this way:
if ($user->isPrivate('email') or $user->isPrivate('phone'))
Or a different implementation (usage is the same)
private $privacyAttributes = null;
public function isPrivate($attribute){
if($this->privacyAttributes == null){
$this->privacyAttributes = $this->privacy()->lists('key');
}
return in_array($attribute, $this->privacyAttributes);
}
User Model header:
/**
* Class User
* #package Interallmas
*/
class User extends Model implements AuthenticatableContract, CanResetPasswordContract {
/**
* #var null|array
*/
protected $privacy_keys = NULL;
Privacy Relationship:
/**
* #return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function privacy() {
return $this->hasMany('Interallmas\Privacy');
}
Privacy functions:
/**
* #return bool
*/
public function privacy_initialized() {
return ($this->privacy_keys !== NULL);
}
/**
* #return void
*/
public function initialize_privacy() {
if (!$this->privacy_initialized()) {
$this->privacy_keys = [];
foreach ($this->privacy as $privacy) {
$this->privacy_keys[] = $privacy->privacy_key;
}
}
}
/**
* #param $key
* #return bool
*/
public function isPrivate($key) {
$this->initialize_privacy();
return (in_array($key,$this->privacy_keys));
}
So: Whenever i access the isPrivate($key) method, i cache the result for the next use so i don't hit the server too hard - the function may be accessed once or more - i just query once, the first time. I believe for my needs, this is the best way to do it.
I think a simple count > 0 check should suffice. This requires you to have defined the relationship with the hasMany method for the User Model.
if (count($user->privacy) > 0) {
...
}

How do you populate an Entity with foreign keys from an Array in Symfony2/doctrine

Consider an entity which has a foreign key. We receive an array which contains the foreign key as an integer and we want to populate the current entity from the array (this could be either an update or a create, makes no difference). So far I have been unable in the documentation to find an example of how to perform this update in the "symfony2/doctrine" way.
If we change the type of the private variable to be an integer instead of an object of the type of the Foreign entity, we lose the ORM mapping/auto-instantiation/etc. If we leave as is, we cannot set it with a simple integer.
Doctrine documentation dictates we shall not access the entity manager from within an Entity (for the purpose of "find"ing the foreign entity from the key before setting current entities value) and anyway I haven't found documentation of how to do it if I wanted to. Best practices dictate that population of an object from an array should occur as an object method on that object. Common sense dictates that a simple array should be supported and should not require end user/controller to know to create the foreign entity.
Can someone point me in the direction of sanity?
Example Code:
<?php
namespace Prefix\MyBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
class Item
{
private $itemid;
/*
* --- omitted for brevity ---
*/
private $categoryid;
public function getItemid()
{
return $this->itemid;
}
/**
* --- omitted for brevity again ---
*/
public function setCategoryid(\Prefix\MyBundle\Entity\Category $categoryid = null)
{
$this->categoryid = $categoryid;
return $this;
}
public function getCategoryid()
{
return $this->categoryid;
}
public function fromArray($data = array())
{
$updated = false;
if ( isset($data['Category']) )
{
/* We know that $data['Category'] will be an int */
$this->setCategoryid($data['Category']); //Fails invalid type!
$updated = true;
}
return $updated;
}
}
So, you have to create your Category Object (owning side, many side), then fetch every Item object from db (you have an array of integer that are IDs, so you can do something like ->findById() or a custom DQL query where you can fetch them into a shot). Subsequently you have to call, for every fetched object, a ->setItems() or ->addItem() and use ->persist(); ->flush(); of entity manager.
When you use doctrine you shouldn't be working with foreign keys (
but with category objects. So in fact you should have converted the id to category outside your function.
Another option is to add a new column called category which is of type integer and you set that directly so that you know have 2 class variables pointing to the same object but one has the relation the other just the id
/**
* #ManyToOne...
*/
protected $category;
/**
* #Column(type="integer")
*/
protected $categoryid;
public function setCategory(\Prefix\MyBundle\Entity\Category $category = null)
{
$this->category = $category;
return $this;
}
public function getCategory()
{
return $this->category;
}
public function setCategoryId($categoryid = null)
{
$this->categoryid = $categoryid;
return $this;
}
public function getCategoryId()
{
return $this->categoryid;
}

Categories