Let's say I want to construct a "player" activerecord object with using some parameters. How can I pass the parameters? Some articles suggest not overriding the __construct method, rather to use the init() function which is supposed to be called internally at the end of the construct method. But how can I pass the parameters to initialize the object?
If I pass an argument to the constructor it complaints me about:
Declaration of app\models\Player::init(app\models\Game $game) should
be compatible with yii\db\BaseActiveRecord::init()
The code looks like this:
Controller:
$game = Game::findOne($id);
$player = new Player($game);
Model:
public function init(Game $game) {
$this->game_id = $game->id;
}
Every object which extends BaseObject you can initialize your object with the $config parameter in the __construct() method.
For example: you can construct your Player or Game object by passing an array with object property parameters, like $game = new Game(['player_id' => 1]).
Related
Is there any possible way to lazy load a custom attribute on a Laravel model without loading it every time by using the appends property? I am looking for something akin to way that you can lazy load Eloquent relationships.
For instance, given this accessor method on a model:
public function getFooAttribute(){
return 'bar';
}
I would love to be able to do something like this:
$model = MyModel::all();
$model->loadAttribute('foo');
This question is not the same thing as Add a custom attribute to a Laravel / Eloquent model on load? because that wants to load a custom attribute on every model load - I am looking to lazy load the attribute only when specified.
I suppose I could assign a property to the model instance with the same name as the custom attribute, but this has the performance downside of calling the accessor method twice, might have unintended side effects if that accessor affects class properties, and just feels dirty.
$model = MyModel::all();
$model->foo = $model->foo;
Does anyone have a better way of handling this?
Is this for serialization? You could use the append() method on the Model instance:
$model = MyModel::all();
$model->append('foo');
The append method can also take an array as a parameter.
Something like this should work...
public function loadAttribute($name) {
$method = sprintf('get%sAttribute', ucwords($name));
$this->attributes[$name] = $this->$method();
}
I have a Accessor method in Collection Model getSizesAttribute, which returns array of available sizes eg: ['S','L'], Now I need to get Models with have size 'S'. like:
$collections = $collections->where('sizes','S');
But sizes is array, could I manipulate this anyhow so that I could check returns only if sizes have specific size.
I tried making another method getIsSizeAttribute, like:
public function getIsSizeAttribute($size){
return in_array($size,$this->sizes);
}
Now How could I user this in Where condition like
$collections = $collections->where('is_size','S');
Mutators and Accessors only run skin-deep, after the query's already been executed. You could use Collection::filter() as Bangnokia suggests, but that wouldn't give you any performance benefit of actually applying the condition to the initial request.
I think what you're looking for here is a Query Scope. Add something like this to your Model class:
public function scopeSize(\Illuminate\Database\Eloquent\Builder $query, $size)
{
return $query->whereIn('sizes', $this->sizes[$size]);
}
And access it like this:
$collection = $model->size('S')->get();
You should use filter on collection
$collections = $collections->filter(function($item, $index) {
return in_array('S', $item->sizes);
});
I have a table 'tour2s' with 2 rows and when I do:
$tour = Tour2::find(1);
dd($tour);
it returns the tour with 'id' = 1. And it's Object.
I want to turn the object to collection of only attributes of the model, nothing else. And I know that when I use ->get() it returns collection.
But when I am trying:
$tour = Tour2::find(1)->get();
dd($tour);
It returns a collection but of all 2 tour objects (full objects, not only attributes):
I did it like:
$tour = Tour2::find(1);
$tour = collect($tour);
dd($tour);
and now it's what i what - it return a collection of only model attributes (WHAT I WANTED):
SO, my question is why when I used $tour=Tour2::find(1)->get() it returned all tours not only the one with 'id'=1 ?
Passing an array to find() will return a collection.
$tour = Tour2::find([1]);
However, it will be a collection of Tour2 objects, not only the attributes.
Then, if you want only the attributes, you could use $tour->toArray()
You could also do $tour = collect(Tour2::find(1));
And to answer your question, when you use $tour=Tour2::find(1)->get(), Laravel fetch the first tour, and then calling get() on $tour will fetch all other records, so return two tours in your case.
Ok, the main question, as i understand is: "Why when i wrote Tour2::find(1)->get() i receives collection of all records".
when you wrote Tour2::find(1) it assumes that you receive instanse of model Tour2. So we can simple write $tourInstanse->get()
If you go to \Illuminate\Database\Eloquent\Model you can see that here is no method called get() but we have a magic method __call. Look at his implementation:
public function __call($method, $parameters)
{
if (in_array($method, ['increment', 'decrement'])) {
return $this->$method(...$parameters);
}
return $this->newQuery()->$method(...$parameters);
}
So, when you call get() method on a model instance you get model`s QueryBuilder (as described in last row) and call get() method on a QueryBuilder. As a result, you receiving all records of that model Class.
This may seem like a trivial question, however all of the obvious solutions that I can think of have their own flaws.
What we want is to be able to set any default ActiveRecord attribute value for new records only, in a way that makes it readable before and during validation and does not interfere with derived classes used for search.
The default values need to be set and ready as soon as we instantiate the class, so that (new MyModel)->attr returns the default attr value.
Here are some of the possibilities and the problems they have:
A) In MyModel override the init() method and assign default value when isNewRecord is true like so:
public function init() {
if ($this->isNewRecord) {
$this->attr = 'defaultValue';
}
parent::init();
}
Problem: Search. Unless we explicitly unset our default attribute in MySearchModel (very error-prone because it is too easy to forget), this will also set the value before calling search() in the derived MySearchModel class and interfere with searching (the attr attribute will already be set so search will be returning incorrect results). In Yii1.1 this was resolved by calling unsetAttributes() before calling search(), however no such method exists in Yii2.
B) In MyModel override the beforeSave() method like so:
public function beforeSave($insert) {
if ($insert) {
$this->attr = 'defaultValue';
}
return parent::beforeSave();
}
Problem: Attribute is not set in unsaved records. (new MyModel)->attr is null. Worse yet, even other validation rules that rely on this value will not be able to access it, because beforeSave() is called after validation.
C) To ensure the value is available during validation we can instead override the beforeValidate() method and set the default values there like so:
public function beforeValidate() {
if ($this->isNewRecord) {
$this->attr = 'defaultValue';
}
return parent::beforeValidate();
}
Problem: Attribute is still not set in unsaved (unvalidated) records. We need to at least call $model->validate() if we want to get the default value.
D) Use DefaultValidator in rules() to set a default attribute value during validation like so:
public function rules() {
return [
[
'attr', 'default',
'value' => 'defaultValue',
'on' => 'insert', // instantiate model with this scenario
],
// ...
];
}
Problem: Same as B) and C). Value is not set until we actually save or validate the record.
So what is the right way to set default attribute values? Is there any other way without the outlined problems?
There's two ways to do this.
$model => new Model();
Now $model has all the default attributes from the database table.
Or in your rules you can use:
[['field_name'], 'default', 'value'=> $defaultValue],
Now $model will always be created with the default values you specified.
You can see a full list of core validators here http://www.yiiframework.com/doc-2.0/guide-tutorial-core-validators.html
This is a hangup with Yii's bloated multi-purpose ActiveRecords
In my humble opinion the form models, active records, and search models would be better off split into separate classes/subclasses
Why not split your search models and form models?
abstract class Creature extends ActiveRecord {
...
}
class CreatureForm extends Creature {
public function init() {
parent::init();
if ($this->isNewRecord) {
$this->number_of_legs = 4;
}
}
}
class CreatureSearch extends Creature {
public function search() {
...
}
}
The benefits of this approach are
You can easily cater for different validation, set up and display cases without resorting to a bunch of ifs and switches
You can still keep common code in the parent class to avoid repetition
You can make changes to each subclass without worrying about how it will affect the other
The individual classes don't need to know about the existence of any of their siblings/children to function correctly
In fact, in our most recent project, we are using search models that don't extend from the related ActiveRecord at all
I know it is answered but I will add my approach.
I have Application and ApplicationSearch models. In Application model I add init with a check of the current instance. If its ApplicationSearch I skip initializations.
public function init()
{
if(!$this instanceof ApplicationSearch)
{
$this->id = hash('sha256', 123);
}
parent::init();
}
also as #mae commented below you can check for existence of search method in current instance, assuming you didn't add any method with name search to the non-search base model so the code becomes:
public function init()
{
// no search method is available in Gii generated Non search class
if(!method_exists($this,'search'))
{
$this->id = hash('sha256', 123);
}
parent::init();
}
I've read your question several times and I think there are some contradictions.
You want the defaults to be readable before and during validation and then you try init() or beforeSave(). So, assuming you just want to set the default values in the model so they can be present during the part of the life cycle as long as possible and not interfere with the derived classes, simply set them after initialising the object.
You can prepare separate method where all defaults are set and call it explicitly.
$model = new Model;
$model->setDefaultValues();
Or you can create static method to create model with all default values set and return the instance of it.
$model = Model::createNew();
Or you can pass default values to constructor.
$model = new Model([
'attribute1' => 'value1',
'attribute2' => 'value2',
]);
This is not much different from setting the attributes directly.
$model = new Model;
$model->attribute1 = 'value1';
$model->attribute2 = 'value2';
Everything depends on how much transparent would you like your model be to your controller.
This way attributes are set for the whole life cycle except the direct initialisation and it's not interfering with derived search model.
Just override __construct() method in your model like this:
class MyModel extends \yii\db\ActiveRecord {
function __construct(array $config = [])
{
parent::__construct($config);
$this->attr = 'defaultValue';
}
...
}
If you want to load default value from database you can put this code in your model
public function init()
{
parent::init();
if(!method_exists($this,'search')) //for checking this code is on model search or not
{
$this->loadDefaultValues();
}
}
You can prepare separate method where all defaults are set and call it explicitly.
$model = new Model;
if($model->isNewRecord())
$model->setDefaultValues();
I'm looking for a way to define a custom Eloquent method which only returns a calculation based on several columns in the according database table. I also tried setting it in the models attributes, but neither seem to work. Maybe I'm missing something, here's what I got so far:
<?php
use Illuminate\Database\Eloquent\Model;
class Team extends Model
{
public function difference() {
return $this->goals_f - $this->goals_a;
}
}
The error message I'm recieving is:
Relationship method must return an object of type Illuminate\Database\Eloquent\Relations\Relation
Your error makes it sound to me like you are trying to call your method like a property" $team->difference. You would instead need to call it like: $team->difference(). If you want to get the difference as if it was a property, then you would need to define an accessor:
public function getDifferenceAttribute(){
return $this->goals_f - $this->goals_a;
}
Then you could call it like $team->difference.
You could also define a protected $appends property to your model so that the difference property is subsequently visible in it's JSON representation:
protected $appends = ['difference'];
More information: https://laravel.com/docs/5.2/eloquent-mutators#accessors-and-mutators