In mysql I can set default value for column like this:
ALTER TABLE <Table> CHANGE <Column> DEFAULT <NEW_DEFAULT_VALUE>
I can retrieve default value:
SELECT DEFAULT(<Column>) FROM <Table> LIMIT 1
Is it possible to achieve this concept with Doctrine?
What I actually need is such methods in my table class:
class UserTable extend Doctrine_Table {
/* ... */
/**
* #return Doctrine_Record
*/
public function getDefaultProperty() {
return ???;
}
public function setDefaultProperty($value) {
/* $value can be either integer or Doctrine_Record */
}
}
Let's see if we can figure this out.
Line 567 of Record.php, part of the assignDefaultValues() method which populates defaults as the object is created, says:
$default = $this->_table->getDefaultValueOf($column);
So you don't actually need to add a getDefaultProperty(), you can use:
$userTable = Doctrine::getTable('User');
$defaultName = $userTable->getDefaultValueOf('name');
However, you seem to want getDefaultProperty() to return a Doctrine_Record. If you just want to get back a whole object that has all the default values set on it, I'm pretty sure you can just use:
$defaultUser = new User(); (See the Doctrine manual for Default Values)
But be sure that your defaults are defined in the schema.yml, not just in the database. Doctrine won't read defaults out of the database, and relies on the schema to tell it what to do.
Let me know how that works.
Cheers,
~J
The Doctrine_Table class contains the getDefaultValueOf method which does what you're looking for. So, if you have a doctrine record, you can write something like: $record->getTable()->getDefaultValueOf('address_line_1'); where 'address_line_1' is a column. Hope this helps.
Related
I am currently moving over from symfony to laravel, it's quite a bit different when it comes to the database. So i have a basic model, i'm just going to use an example:
class Test extends Model
{
use HasFactory;
}
All good, i have a migration and the table created. However, i don't like this:
$test = new Test();
$test->my_field = 'hello';
$test->save();
I don't like it because it's having to use a magic __set() to create the parameter, if i define the parameter in my model like this:
class Test extends Model
{
use HasFactory;
public ?string $my_field;
}
I get database errors when it tries to insert when i define the params like this. Why is that? It's doing the same thing as __set() but i'm actually physically defining them, which in my opinion is a better way to code it as my IDE can typehint and it's just nicer to follow the program knowing what params are there.
What's the reason for it inserting when i don't define them, and not when i do? From my actual table which is bookings , has a field booking_ref:
General error: 1364 Field 'booking_ref' doesn't have a default value (SQL: insert into booking_reviews (updated_at, created_at) values (2021-12-13 14:13:08, 2021-12-13 14:13:08))
This happens when i define the $booking_ref param on the model, but if i take it out and rely on the __set() method it works fine. Doesn't make any sense to me right now.
I think this is a reasonable enough misunderstanding to be useful to future visitors, so I want to try to explain what's going on with some pseudo-code and some references to the current source code.
You are correct that when setting a property on a Laravel model, that is a column in the DB, internally Laravel is using the PHP magic method __set.
What this does is allow you to 1) set properties directly instead of calling some kind of setter function, and 2) interact with your table columns without needing the boilerplate of column definitions in your model.
Where the assumptions go wrong is with what __set is doing. __set does not have to simply set an actual property with the same name. __set is just a method you may implement to do whatever you want. What you assumption implies is that it's doing something like this:
public function __set($key, $value)
{
$this->{$key} = $value;
}
However, you can do whatever you want with the $key and $value passed to the magic method.
What Laravel does is call another method defined in the HasAttributes trait - setAttribute.
public function __set($key, $value)
{
$this->setAttribute($key, $value);
}
setAttribute does a few extra things, but most importantly it adds the key/value pair to Model property $this->attributes[].
To hopefully help this difference make sense, here is what the two __set methods would yield with a basic example:
$model->my_column = 'value';
// 1st example
/**
* {
* public $my_column = 'value';
* }
*/
// Laravel way
/**
* {
* protected $attributes= ['my_column => 'value'];
* }
*/
I won't go through both saving and updating since they're very similar, but to show how this is used, we can look at the save method, which calls performInsert and after a few more calls makes it's way back to the attributes property to determine what to actually insert into the query.
Summary
Laravel does not use custom model properties when deciding what column/values to add to queries.
This is why when you create custom mutators, you interact with the attributes property just like Laravel does internally.
Anytime you introduce "magic" into code, you have some tradeoffs. In this case, that tradeoff is slightly less clarity with what database columns are actually available. However, like I mentioned in comments, there are other solutions to make models more IDE friendly like Laravel IDE helper.
I have a following table structure in my existing database :
tbl_Grzyby (ID, Nazwa, ...) - main table,
tblk_TypGrzyba (ID, TypGrzyba, OrderNo) - lookup table,
tblm_TypGrzyba (ID_Grzyb, ID_TypGrzyba) (many-to-many table).
I've created a grzyby Eloquent model class, which has a custom property, defined as:
class grzyby extends Model
{
protected $table = 'tbl_Grzyby';
public function typ_grzyba() {
return $this->belongsToMany('\App\Lookups\typ_grzyba', 'tblm_TypGrzyba','ID_Grzyb', 'ID_TypGrzyba');
}
}
I've also created the lookup table model, defined as:
class typ_grzyba extends Model {
protected $table = 'tblk_TypGrzyba';
public function grzyby() {
return $this->belongsToMany('\App\grzyby', 'tblm_TypGrzyba','ID_Grzyb', 'ID_TypGrzyba');
}
}
When I try with tinker to get record from main with ID=67, like:
$main_record=\App\grzyby::where('ID',67)->first();
it returns me all the details for that record fine. Returning all lookups runs also fine:
$types = \App\Lookups\typ_grzyba::all();
However, if I try to do the following:
$main_record_types = $main_record->typ_grzyba;
tinker returns me an empty collection, despite record existing in the many to many table:
When I run the query log it outputs me following SQL query:
select `tblk_TypGrzyba`.*, `tblm_TypGrzyba`.`ID_Grzyb` as `pivot_ID_Grzyb`, `tblm_TypGrzyba`.`ID_TypGrzyba` as `pivot_ID_TypGrzyba` from `tblk_TypGrzyba` inner join `tblm_TypGrzyba` on `tblk_TypGrzyba`.`id` = `tblm_TypGrzyba`.`ID_TypGrzyba` where `tblm_TypGrzyba`.`ID_Grzyb` is null
If I run that query against the database it returns me empty result, however, if I change the where clause manually to
where `tblm_TypGrzyba`.`ID_Grzyb` = 67
I'm getting the expected results in phpMyAdmin.
What's the reason of it and how I can fix that?
The solution was to add the primaryKey overrride because I was using uppercase for the ID field and then restart tinker.
You mixed up your foreign key defintion in your relationships. You wrote this twice:
return $this->belongsToMany('\App\grzyby', 'tblm_TypGrzyba','ID_Grzyb', 'ID_TypGrzyba');
I assume copy pasting? in the second model, justt swap the foreign key. Foreign key of the current model comes first.
In typ_grzyba model, do
public function grzyby() {
return $this->belongsToMany('\App\grzyby', 'tblm_TypGrzyba', 'ID_TypGrzyba', 'ID_Grzyb');
}
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 noticed that I write the database table names quite a lot, and in different files, when I use the Query Builder. If I were to change the database table names, I would have to search and change quite many rows in my project.
Is this an issue your Laravel guys noticed and come up with an solution to?
I like the Eloquent approach which uses class models, instead of database names; but for some queries I think the Query Builder is a better solution (though I am no expert in this matter).
If you already have a queryBuilder object you can obtain the table name like
$tableName = $query->getModel()->getTable();
Use this in your query :
(new YourModel())->getTable()
Example :
DB:raw('SELECT * FROM '.(new User())->getTable().' WHERE id=3');
How about using OOP concept. Laravel is a framework, so no one stops you from using basic PHP OOP concept. This is what I do:
Consider my query is like :
$result=DB::table('myTable')->select()->get();
What I do is make a class that holds all the tablenames :
class TableName
{
private $tableName= "myTable";
public function getTableName()
{
return $this->tableName;
}
public function setTableName($table_name)
{
$this->tableName = $table_name;
}
}
Now all i have to do is call a method using an object in the file I want to use the table like :
$name = new TableName() ;
$result=DB::table($name->getTableName())->select()->get();
Use wherever you want. I don't think its the best solution however it works for me. Hope it helps
Maybe you can extend the model class.
CModel extend Model {
protected static $tableName;
public static getTableName(){
if(static::$tableName)
return static::$tableName;
/* if you create a "reference break" you don't have to *
/* create "protected static $tableName" row in your all model */
$table = (new static())->getTable();
return static::$tableName = &$table;
}
}
YourModel extends CModel {...}
than you can use
YourModel::getTableName()
I'm not have better idea.
I thought I know every aspect of Yii2 in the meantime, but this one gives me headaches.
Situation
Two tables: Client and Billings. The Client-Table holds a regular list of clients. The Billing-table has several entries for each client (1:n).
Problem
I want to fetch a calculated DB-Field together with the row itself and access it via a virtual property of the model.
Key is that it gets calculated and selected together with the row itself. I know I can achieve something similliar with a regular virtual getter calculating the amount...but this is not at the same time as the select itself.
My Plan
In the query-object of the client-model i tried to add an an additional select (addSelect-Method) and give the field an alias. Then I added the alias of this select with the attributes-method of the model. Somehow this didn't work.
My Question
Does someone of you know the right way to achieve this? As this is a very common problem, I can not imagine this beeing too hard. I just somehow can't find the solution.
Sample code:
echo $client->sumOfBillings should output the contents of the corresponding property within the client-model. The contents of this property should be filled when fetching the client-row itself and not at the moment the property gets called.
I actual found the answer myself. Here is how you do it:
Query object
The fetching of all the Yii2-Models is done via their corresponding Query-Object. This object is retrieved via the models find()-Method. If you override this method, you can return your own query-object for that class. In the example above my model looks like this:
class Client extends \yii\db\ActiveRecord
{
//...
public static function find()
{
return new ClientQuery(get_called_class());
}
//...
}
Now within the Query-Objects init()-Method we can add the corresponding additional selects:
public class ClientQuery extends \yii\db\ActiveQuery
{
public function init()
{
parent::init();
//prepare subquery for calculation
$sub = (new Query())
->select('SUM(billing_amount)')
->from('billing')
->where('billing.client_id = client.id');
$this->addSelect(['client.*', 'sumBillings'=>$sub]);
}
}
We are now done with the query-Object. What have we done now? When selecting a client the sum gets calculated and loaded as well. But how do we access it? This was the hard part where I struggeled. The solution lies within the ActiveRecord-class.
Possibilities to populate the model with calculated data
There are several possibilities to load this data into the model-class. To understand what options we have, we can check out the populateRecord($record, $row)-method of the BaseActiveRecord-class:
/**
* Populates an active record object using a row of data from the database/storage.
*
* This is an internal method meant to be called to create active record objects after
* fetching data from the database. It is mainly used by [[ActiveQuery]] to populate
* the query results into active records.
*
* When calling this method manually you should call [[afterFind()]] on the created
* record to trigger the [[EVENT_AFTER_FIND|afterFind Event]].
*
* #param BaseActiveRecord $record the record to be populated. In most cases this will be an instance
* created by [[instantiate()]] beforehand.
* #param array $row attribute values (name => value)
*/
public static function populateRecord($record, $row)
{
$columns = array_flip($record->attributes());
foreach ($row as $name => $value) {
if (isset($columns[$name])) {
$record->_attributes[$name] = $value;
} elseif ($record->canSetProperty($name)) {
$record->$name = $value;
}
}
$record->_oldAttributes = $record->_attributes;
}
As you can see, the method takes the raw-data ($row) and populates the model instance ($record). If the model has either a property or a setter-method with the same name as the calculated field, it will be populated with data.
Final code of Client-Model
This is my final code of the Client-model:
class Client extends \yii\db\ActiveRecord
{
private $_sumBillings;
//...
public static function find()
{
return new ClientQuery(get_called_class());
}
public function getSumBillings()
{
return $this->_sumBillings;
}
protected function setSumBillings($val)
{
$this->_sumBillings = $val;
}
//...
}
The populateRecord()-method will find the setter-method ($record->canSetProperty($name)) and call it to fill in the calculated value. As it is protected, it is otherwise readonly.
VoilĂ ...not that hard actually and definitely useful!