One Eloquent model from multiple databases - php

I'm working on a use case where one Eloquent Model should be found in multiple databases (eg. from different installations of an application).
The only way I can think of is using a super class and inherit from it with different protected $connection = 'database_connection''s. Like this:
class SuperModel extends Model
{
...
}
class SubModel1 extends SuperModel
{
protected $connection = 'connection_1';
...
}
class SubModel2 extends SuperModel
{
protected $connection = 'connection_2';
...
}
Is this the correct way to go or am I missing some Eloquent functionality that allows for this?

Related

Laravel connection attribute ignored with hasmanythrough

Summary
I am trying to use hasManyThrough relationship with tables from different connections.
Description of the problem:
When defining a hasManyThrough relationship, the protected $connection attribute is ignored.
Steps To Reproduce:
Create the following models (Abbreviated code)
class Resource extends Model
{
protected $connection = 'tcollect'
public function absences()
{
return $this->hasManyThrough('ARM\TargetHoraire\Absence', 'ARM\Tcollect\ICO\ICOExternalReference', 'RecordID', 'ResourceID', 'ID', 'ExternalKey');
}
}
class Absence extends Model
{
protected $table = 'Absences';
protected $connection = 'punch';
}
class ICOExternalReference extends Model
{
protected $table = 'ICOExternalReferences';
protected $connection = 'tcollect';
}
Then call the relationship on the resource model
$resource->absences;
Notice that ICOExternalReference $connection attribute is ignored. It tries to use the ICOExternalReference from the punch connection.
Question
Is there any problem with my code or is there aworkaround to solve this problem?
I have created an issue on the framework repo, but it got closed right away.
Try with
hasMany(class with all parameter)

Laravel eloquent build child object from parent with mongo

I am using laravel 5.1 and jenssegers mongodb and I am having some issues with such structure
class ServiceProvider extends Eloquent {
protected $collection = 'service_provider';
protected $connection = 'mongodb';
public static function factory($serviceId) {
switch ($serviceId) {
case self::SERVICE_PROVIDER_CHILD_CARE : {
return new ChildCareServiceProvider();
}
break;
}
}
public static get_by_service_id($service_id) {
return self::find($serviceId)->first();
}
}
class ChildCareServiceProvider extends ServiceProvider implements IServiceProvider
{
protected $collection = 'service_provider';
protected $connection = 'mongodb';
public function availabilityTimes()
{
return $this->hasMany('App\Models\ServiceProvider\ServiceProviderAvailabilityTime');
}
}
When I am saving or updating service provider I know what kind of service it is , so I can use factory method to get child and save it. But when I am getting service by id - I dont know service type yet, I only know its id. So I do have a method in ServiceProvider which makes query to mongo collection and getting record by its id. In this case that record will be an instance of object ServiceProvider. Is there an easy way to create ChildCareServiceProvider object from ServiceProvider object data? I tried something like
$data = ServiceProvider::find($serviceId)->first()->attributesToArray();
$serviceProvider = new ChildCareServiceProvider($data);
but in this case $serviceProvider object internal structure is little bit different in terms of its internal properties, which somehow affects my availabilityTimes relationship
I am sorta new to laravel and mongo, any advice would be greatly appreciated
Take a look at PHP's magic methods. What you can do is set the entity and then for example the __get() and __set() methods, set/get the value from/to the entity.

Traits with PHP and Laravel

I am using Laravel 5.1 and would like to access an array on the Model from the Trait when the Model before the model uses the appends array.
I would like to add certain items to the appends array if it exists from my trait. I don't want to edit the model in order to achieve this. Are traits actually usable in this scenario or should I use inheritance?
array_push($this->appends, 'saucedByCurrentUser');
Here is how my current setup works.
Trait
<?php namespace App;
trait AwesomeSauceTrait {
/**
* Collection of the sauce on this record
*/
public function awesomeSauced()
{
return $this->morphMany('App\AwesomeSauce', 'sauceable')->latest();
}
public function getSaucedByCurrentUserAttribute()
{
if(\Auth::guest()){
return false;
}
$i = $this->awesomeSauced()->whereUserId(\Auth::user()->id)->count();
if ($i > 0){
return true;
}
return false;
}
}
Model
<?php namespace App;
use App\AwesomeSauceTrait;
use Illuminate\Database\Eloquent\Model;
class FairlyBlandModel extends Model {
use AwesomeSauceTrait;
protected $appends = array('age','saucedByCurrentUser');
}
What I would like to do is something to achieve the same effect as extending a class. I have a few similar traits, so using inheritance gets somewhat ugly.
trait AwesomeSauceTrait {
function __construct() {
parent::__construct();
array_push($this->appends, 'saucedByCurrentUser');
}
}
I have seen some workarounds for this, but none of them seem better/cleaner than just adding the item to the array manually. Any ideas are appreciated.
Update
I discovered this way of accomplishing what I need for one trait, but it only works for one trait and I don't see an advantage of using this over inheritance.
trait
protected $awesomeSauceAppends = ['sauced_by_current_user'];
protected function getArrayableAppends()
{
array_merge($this->appends, $this->awesomeSauceAppends);
parent::getArrayableAppends();
}
How I am currently handling my Model, for what it is worth.
model
public function __construct()
{
array_merge($this->appends, $this->awesomeSauceAppends);
}
Traits are sometimes described as "compiler-assisted copy-and-paste"; the result of using a Trait can always be written out as a valid class in its own right. There is therefore no notion of parent in a Trait, because once the Trait has been applied, its methods are indistinguishable from those defined in the class itself, or imported from other Traits at the same time.
Similarly, as the PHP docs say:
If two Traits insert a method with the same name, a fatal error is produced, if the conflict is not explicitly resolved.
As such, they are not very suitable for situations where you want to mix in multiple variants of the same piece of behaviour, because there is no way for base functionality and mixed in functionality to talk to each other in a generic way.
In my understanding the problem you're actually trying to solve is this:
add custom Accessors and Mutators to an Eloquent model class
add additional items to the protected $appends array matching these methods
One approach would be to continue to use Traits, and use Reflection to dynamically discover which methods have been added. However, beware that Reflection has a reputation for being rather slow.
To do this, we first implement a constructor with a loop which we can hook into just by naming a method in a particular way. This can be placed into a Trait of its own (alternatively, you could sub-class the Eloquent Model class with your own enhanced version):
trait AppendingGlue {
public function __construct() {
// parent refers not to the class being mixed into, but its parent
parent::__construct();
// Find and execute all methods beginning 'extraConstruct'
$mirror = new ReflectionClass($this);
foreach ( $mirror->getMethods() as $method ) {
if ( strpos($method->getName(), 'extraConstruct') === 0 ) {
$method->invoke($this);
}
}
}
}
Then any number of Traits implementing differently named extraConstruct methods:
trait AwesomeSauce {
public function extraConstructAwesomeSauce() {
$this->appends[] = 'awesome_sauce';
}
public function doAwesomeSauceStuff() {
}
}
trait ChocolateSprinkles {
public function extraConstructChocolateSprinkles() {
$this->appends[] = 'chocolate_sprinkles';
}
public function doChocolateSprinklesStuff() {
}
}
Finally, we mix in all the traits into a plain model, and check the result:
class BaseModel {
protected $appends = array('base');
public function __construct() {
echo "Base constructor run OK.\n";
}
public function getAppends() {
return $this->appends;
}
}
class DecoratedModel extends BaseModel {
use AppendingGlue, AwesomeSauce, ChocolateSprinkles;
}
$dm = new DecoratedModel;
print_r($dm->getAppends());
We can set the initial content of $appends inside the decorated model itself, and it will replace the BaseModel definition, but not interrupt the other Traits:
class ReDecoratedModel extends BaseModel {
use AppendingGlue, AwesomeSauce, ChocolateSprinkles;
protected $appends = ['switched_base'];
}
However, if you over-ride the constructor at the same time as mixing in the AppendingGlue, you do need to do a bit of extra work, as discussed in this previous answer. It's similar to calling parent::__construct in an inheritance situation, but you have to alias the trait's constructor in order to access it:
class ReConstructedModel extends BaseModel {
use AppendingGlue { __construct as private appendingGlueConstructor; }
use AwesomeSauce, ChocolateSprinkles;
public function __construct() {
// Call the mixed-in constructor explicitly, like you would the parent
// Note that it will call the real parent as well, as though it was a grand-parent
$this->appendingGlueConstructor();
echo "New constructor executed!\n";
}
}
This can be avoided by inheriting from a class which either exists instead of the AppendingGlue trait, or already uses it:
class GluedModel extends BaseModel {
use AppendingGlue;
}
class ReConstructedGluedModel extends GluedModel {
use AwesomeSauce, ChocolateSprinkles;
public function __construct() {
// Standard call to the parent constructor
parent::__construct();
echo "New constructor executed!\n";
}
}
Here's a live demo of all of that put together.
I thought I'd add an update for 2019 since this was one of the first discussions that popped up when trying to do a similar thing. I'm using Laravel 5.7 and nowadays Laravel will do the reflection that IMSoP mentioned.
After the trait has been booted, Laravel will then call initializeTraitName() on the constructed object (where TraitName is the full name of the trait).
To add extra items to $appends from a trait, you could simply do this...
trait AwesomeSauceTrait {
public function initializeAwesomeSauceTrait()
{
$this->appends[] = 'sauced_by_current_user';
}
public function getSaucedByCurrentUserAttribute()
{
return 'whatever';
}
}
KISS:
I don't see any reason why you should use trait when your are simply appending attributes.
I would only recommend using trait without a constructor like you were doing, only if you model is getting pretty bulky and you wish to slim down things.
Please also note this not the correct way of appending attribute
protected $appends = array('age','saucedByCurrentUser');
You could do this:
protected $appends = array('age','sauced_by_current_user');
Appends attribute names should the snake_case of its method Name
Edited:
The idea behind appends is to dynamically add fields that doesn't exist in your database table to your model so after you can do like:
$model = FairlyBlandModel ::find(1);
dd($model->sauced_by_current_user);

$useDbConfig for all uses models at once in Controller for CakePHP v2.x.x

I am currently writing the separate lines for all the models defined in Public $uses = array('Lead', 'User', 'Source', ...) to use the $useDbConfig in Controller.
$this->Lead->useDbConfig = $newDbConfig['name'];
$this->User->useDbConfig = $newDbConfig['name'];
$this->Source->useDbConfig = $newDbConfig['name'];
But i want to set $useDbConfig for all $uses ->useDbConfig at once.
With foreach loop it isn't seems to achieve. Is there any way to achieve it?
Cake v2.5.7
Assuming you always want your Lead, User, etc. models to use the alternative db config the way I'd approach this is to create a new AppModel for these models that changes useDbConfig and extends AppModel:-
class AltAppModel extends AppModel {
public $useDbConfig = 'your-alt-db-config';
}
Then for your models that require the alternative config extend the new AppModel instead
class Lead extends AltAppModel {
}
Obviously use a better naming convention than AltAppModel that's more suited to your project. :-)
Update
Not sure if this would work or not, but based on your comments you could try something like this:-
class AppModel extends Model {
public function changeDbConfig($config) {
$this->useDbConfig = $config;
// Update the belongs to associates
foreach ($this->belongsTo as &$Model) {
$Model->useDbConfig = $config;
}
// Then repeat for hasOne and hasMany...
}
}
Then when you need to dynamically change $useDbConfig call:-
$this->Lead->changeDbConfig('your-alt-db-config');
I'm not 100% sure that this would work though as Cake may not let you update these in this way. Dynamically changing $useDbConfig feels a little quirky to me. If you're changing it based on the app setup it would be better to change this in bootstrap.
Yep, In your Controller you can iterate in the uses array, for example:
public $uses = ['Lead', 'User', 'Source'];
foreach ($this->uses as $model) {
$this->{$model}->setDataSource($newDbConfig['name']);
}

How to replace the Laravel Builder class

I want to replace the Laravels builder class with my own that's extending from it. I thought it would be as simple as matter of App::bind but it seems that does not work. Where should I place the binding and what is the proper way to do that in Laravel?
This is what I have tried:
my Builder:
use Illuminate\Database\Eloquent\Builder as BaseBuilder;
class Builder extends BaseBuilder
{
/**
* Find a model by its primary key.
*
* #param mixed $id
* #param array $columns
* #return \Illuminate\Database\Eloquent\Model|static|null
*/
public function find($id, $columns = array('*'))
{
Event::fire('before.find', array($this));
$result = parent::find($id, $columns);
Event::fire('after.find', array($this));
return $result;
}
}
And next I tried to register the binding in bootstrap/start.php file like this :
$app->bind('Illuminate\\Database\\Eloquent\\Builder', 'MyNameSpace\\Database\\Eloquent\\Builder');
return $app;
Illuminate\Database\Eloquent\Builder class is an internal class and as such it is not dependency injected into the Illuminate\Database\Eloquent\Model class, but kind of hard coded there.
To do what you want to do, I would extend the Illuminate\Database\Eloquent\Model to MyNamespace\Database\Eloquent\Model class and override newEloquentBuilder function.
public function newEloquentBuilder($query)
{
return new MyNamespace\Database\Eloquent\Builder($query);
}
Then alias MyNamespace\Database\Eloquent\Model to Eloquent at the aliases in app/config/app.php
Both of the answers are correct in some way. You have to decide what your goal is.
Change Eloquent Builder
For example, if you want to add a new method only for eloquent models (eg. something like scopes, but maybe a little more advanced so it’s not possible in a scope)
Create a new Class extending the Eloquent Builder, for Example CustomEloquentBuilder.
use Illuminate\Database\Eloquent\Builder;
class CustomEloquentBuilder extends Builder
{
public function myMethod()
{
// some method things
}
}
Create a Custom Model and overwrite the method newEloquentBuilder
use Namespace\Of\CustomEloquentBuilder;
use Illuminate\Database\Eloquent\Model;
class CustomModel extends Model
{
public function newEloquentBuilder($query)
{
return new CustomEloquentBuilder($query);
}
}
Change Database Query Builder
For example to modify the where-clause for all database accesses
Create a new Class extending the Database Builder, for Example CustomQueryBuilder.
use Illuminate\Database\Query\Builder;
class CustomQueryBuilder extends Builder
{
public function myMethod()
{
// some method things
}
}
Create a Custom Model and overwrite the method newBaseQueryBuilder
use Namespace\Of\CustomQueryBuilder;
use Illuminate\Database\Eloquent\Model;
class CustomModel extends Model
{
protected function newBaseQueryBuilder()
{
$connection = $this->getConnection();
return new CustomQueryBuilder(
$connection, $connection->getQueryGrammar(), $connection->getPostProcessor()
);
}
}
Laravel Version: 5.5 / this code is untestet
The answer above doesn't exactly work for laravel > 5 so I done some digging and I found this!
https://github.com/laravel/framework/blob/5.2/src/Illuminate/Database/Eloquent/Model.php#L1868
use this instead!
protected function newBaseQueryBuilder()
{
$conn = $this->getConnection();
$grammar = $conn->getQueryGrammar();
return new QueryBuilder($conn, $grammar, $conn->getPostProcessor());
}

Categories