Populating Yii2 Model via behavior - php

I'm trying to assign a value to the property with behavior, but no matter what value I pass to $this->owner->property the model assigns this number "127" to the property and saves it. I can't figure out where this number comes from.
namespace common\behaviors;
use yii\db\ActiveRecord;
use yii\base\Behavior;
class MyBehavior extends Behavior
{
public function events()
{
return [
ActiveRecord::EVENT_BEFORE_INSERT => 'test',
ActiveRecord::EVENT_BEFORE_UPDATE => 'test',
];
}
public function test()
{
$this->owner->property = 444;
}
}
If I populate this property htrough the web form on frontend the model saves correct value.
I added property to model's rules but that doesn't make any difference.

Shame on me, I made so stupid mistake while creating SQL table :( I assigned tinyint type to property field, that's why it always saves 127 - the maximum allowed value for this type of field.

Related

Change class property value in method A and access updated value in method B

I'm using a custom PHP framework which is largely based on CodeIgniter.
In one of my controller files, I've set a class property called $orderId. After the user has filled in the form and submitted, I'll do a DB insert, get the order no. and override the $orderId class property value with that order no.
Then I'll redirect to the submit page where I want to access that updated $orderId class property value. This final part is not working, the submit class gets a blank value for property $orderId.
Where am I going wrong pls? The basic example below. Maybe I can't do this because of the redirect and should use a session var instead?
Thanks!
[EDIT] Or I could pass the orderId as the 3rd URL param in the redirect. E.G. redirect('orders/submit/'.self::$orderId); in which case I'll turn all the self:: instances into $this-> for class level scope.
class Orders extends Controller {
private static $orderId;
public function __construct() {
// assign db model
}
public function index() {
if($_SERVER['REQUEST_METHOD'] == 'POST') {
$data = [
// form data to pass to db model
];
self::$orderId = 12345; // example order no. return from db model
if(!empty(self::$orderId)) {
redirect('orders/submit');
}
}
}
public function submit() {
$data = [
'orderId' => self::$orderId
];
$this->view('orders/submit', $data);
}
}
The issue itself is a fundamental architecture problem. static only works when you're working with the same instance. But since you're redirecting, the framework is getting reinitialised. Losing the value of your static property. Best way to go about doing this is by storing the order id in a session variable and then read that variable. Sessions last for the as long as the browser window is open

How to solve Laravel select queries conflicting with the $appends property on models?

I have a situation where I need a specific attribute accessor appended to one of my models automatically:
class Mission extends Eloquent {
protected $appends = ['launch_date_time'];
public function getLaunchDateTimeAttribute() {
return ($this->attributes['launch_approximate'] == null) ? $this->attributes['launch_exact'] : $this->attributes['launch_approximate'];
}
}
As you can see, this launch_date_time property is dependent on two other fields of my model that are actually in my database.
However, I now want to perform a query where only a certain number of fields are returned, as this is going to be sent over AJAX multiple times and I would rather use as few resources as possible:
// AJAX GET
// missions/all
public function all() {
$allMissions = Mission::with('featuredImage')->get(['mission_id', 'name', 'featured_image']);
return Response::json($allMissions);
}
The issue here is that I no longer need the launch_date_time attribute, so I have excluded it, **in doing so, my AJAX request does not work successfully:
Undefined index: launch_approximate on line 78 of H:\myproj\app\models\Mission.php
This is clearly because my model is attempting to append launch_date_time, of which launch_approximate is a dependency of. If I include all the required dependencies, all of them any my attribute that I want to append appear:
$allMissions = Mission::with('featuredImage')->get(['mission_id', 'name', 'featured_image', 'launch_approximate', 'launch_exact', 'launch_date_time']);
This is undesirable. Is there a solution where I can keep both setups?
The reason it is not working is because you are not retrieving the required fields from the database in the get method on your query. That is why you can't access launch_exact and launch_approximate because they are not set in the instance of your model.
So to make it work like you want. You would have to check if launch_exact and launch_approximate are set before you access them.
public function getLaunchDateTimeAttribute() {
if(isset($this->attributes['launch_approximate']) && $this->attributes['launch_exact']) {
return ($this->attributes['launch_approximate'] == null) ? $this->attributes['launch_exact'] : $this->attributes['launch_approximate'];
}
return null;
}
You can also set a whitelist with the $visible property and a black list with $hidden inside your model to not show certain attributes when outputing to json or a array take a look at the docs: http://laravel.com/docs/5.1/eloquent-serialization#hiding-attributes-from-json

Cannot access Eloquent attributes on Twig

I am trying to access an Eloquent attribute with Twig in Slim, and getting an error.
I have a Field and a Type object, and the relationship is as follows
class Field extends \Illuminate\Database\Eloquent\Model {
protected $table = 'fields';
public function type()
{
return $this->belongsTo('models\Type');
}
When doing {{ f }} (being f a field), the output is this:
{"field_id":"1","field_name":"Your name","form_id":"2","type_id":"1","placeholder":"Please give us your name"}
And when doing {{ f.type }}, the result is:
Message: An exception has been thrown during the rendering of a template ("Object of class Illuminate\Database\Eloquent\Relations\BelongsTo could not be converted to string") in "pages/editform.html" at line 97.
If I try to do {{ f.type.name }}, doesn't throw up an exception but doesn't print anything either.
If I do it in PHP
$fields = $form->fields;
var_dump($fields[0]->type->name);
The value gets output correctly.
Any ideas?,
Thanks
If you don't want to do an eager load, you can override the magic __isset method in your Field class to return true for the type relationship property:
public function __isset($name)
{
if (in_array($name, [
'type'
])) {
return true;
} else {
return parent::__isset($name);
}
}
Explanation
The problem is in the interaction between how Eloquent implements the "dynamic property" for a relation (in your case, f.type), and the rules that Twig uses for accessing "attributes" of a variable.
From the Twig documentation:
For convenience's sake foo.bar does the following things on the PHP layer:
check if foo is an array and bar a valid element;
if not, and if foo is an object, check that bar is a valid property;
if not, and if foo is an object, check that bar is a valid method (even if bar is the constructor - use __construct() instead);
if not, and if foo is an object, check that getBar is a valid method;
if not, and if foo is an object, check that isBar is a valid method;
if not, return a null value.
foo['bar'] on the other hand only works with PHP arrays:
check if foo is an array and bar a valid element;
if not, return a null value.
The key here is the part where it says "check that bar is a valid property". This means that on the level of PHP, Twig is calling isset on $f->type. Eloquent implements the magic __isset method in Model, so you might think that this wouldn't be a problem.
However, take a look at how it actually implements __isset for model relations:
/**
* Determine if an attribute exists on the model.
*
* #param string $key
* #return bool
*/
public function __isset($key)
{
return (isset($this->attributes[$key]) || isset($this->relations[$key])) ||
($this->hasGetMutator($key) && ! is_null($this->getAttributeValue($key)));
}
It determines whether a relation is "set" by looking in its array of loaded relationships (isset($this->relations[$key])). The problem is that if type hasn't been loaded yet, Eloquent will say that it is not "set".
Thus when Twig looks at $f->type, it will think that type is not a valid property, and move on to the next rule:
...if foo is an object, check that bar is a valid method
So now it will look for the method type(), which it finds. The only problem? type() (the method) returns a Relation (specifically, a BelongsTo), rather than a Type object. And BelongsTo objects aren't models.
If you want Twig to know that the property $f->type does indeed exist, you have two choices:
You can eager-load the related Type object along with your Field, as suggested by #roger-collins, or;
You can overload the __isset magic method in Field:
public function __isset($name)
{
if (in_array($name, [
'type'
])) {
return true;
} else {
return parent::__isset($name);
}
}
This will force Twig to recognize that type is a valid property for Field, even before the related model has actually been loaded.
I had the same issue and stumbled upon this question. After solving it myself, I thought I'd try to help you out.
Try doing a Eager Load on the model:
Field::with('type')->get()
This should allow you to do the following with no other issues.
{{ f.type }}
See more info here: http://laravel.com/docs/4.2/eloquent#eager-loading

Extending Yii model with predefined value

I have a model class with some basic values, and now I want to extend it with a calculated ID field. In my system we use an ID for every entity, that is containing the type of the entity and the auto-increment id from the DB.
I would need a parameter, call it now $cid (calculated id) that is setted when it initialized.
I've tried to set it in the init/model functions, but I get Property "Product.cid" is not defined. Exception.
And I've tried to create a function:
public function _cid($value = null) {
if($value == null){
return $this->cid;
}else{
$this->cid = $value;
return $this->cid;
}
}
How should I extend my model to have this value as a parameter of the Model?
Update
Jon answered really well and the official docs are really helpful. But, with this solution, now the getCid function is only called, when I call it independently. When I call it via the model's getAttributes($model->safeAttributeNames) (or getAttributes(array('cid'))), I get null as the value of $model->cid and the getCid method is not called. (attribute is setted to be safe)
Why don't you simply use a read-only property?
private $_cid;
public function getCid()
{
if ($this->_cid) === null {
// calculate the value here on demand
$this->_cid = 'whatever';
}
return $this->_cid;
}
Thanks to the implementation of __get in CComponent, you can access this value as a property with $model->cid.

cakephp - modifying and saving data

I've been trying to learn cakephp recently but I'm struggling to find any tutorials that deal with storing data into a table after it's been modified. I'm used having complete control where everything goes in PHP, so it's been a struggle adjusting to the automated processe of MVC.
I thought a good first experiment would be to take an input and concatenate a letter to it(let's just say "m"). Then, store both the original value and the concatenated value in a table with fields "orignal" and "concatenated". So, if I typed "hello", the value in the original field would be "hello" and the concatenated field would be "hellom".
My question is would the model be responsible for concatenating the original value? Would it also do the saving or is that the controllers responsibility?
Here is my code: I'm getting the following error.
Fatal error: Call to a member function save() on a non-object in /Applications/XAMPP/xamppfiles/htdocs/cake/app/Model/Concatenate.php on line 6
View/Concatenates/add.php
<h1>Add Something</h1>
<?php
echo $this->Form->create('Concatenate');
echo $this->Form->input('original');
echo $this->Form->end('Add Numbers');
?>
Now for the model
class Concatenate extends AppModel {
function saveConcat($original,$concatenated) {
$this->set(array(
'original' => $original,
'concatenated' => $concatenated));
$this->save();
}
}
?>
Now for the controller
<?php
class ConcatenatesController extends AppController {
public $helpers = array('Html', 'Form');
public $components = array('Session');
public function index() {
$this ->set('concatenates', $this->Concatenate->find('all'));
}
public function add() {
if ($this->request->is('post')) {
$original = $this->request->data['Concatenate']['original'];
$concatenated = $original."m" ;
$this->Concatenate->saveConcat($original,$concatenated);
}
}
function isempty(){ //used to check if there is data in the table. If there isn't any, "no data" will be displayed
$mysorts = $this->Concatenate->find('all');
$this->set('concatenates', $mysorts);
}
}
?>
This is the never ending debate (or preference) about fat model/skinny controller and vice versa.
As far as saving goes, the model should definitely handle the logic for that. Although, you would most likely call it from the controller like $myModel->save($data);
In concatenating values, I would personally handle that in the controller because it is business logic that isn't directly related to the model. For example, you may wish to concatenate a string and send it to the view instead.
[EDIT]
Disclaimer: I have almost zero experience with CakePHP but the fundamentals are the same.
You mentioned you can't get it to work, so one thing I am noticing is you have a function called Concatenate() in your model. This is the PHP4 style of constructors and is no longer "best practice" (unless of course you are running PHP4 but why on earth would you be doing that). In fact, it is likely to be deprecated entirely in the near future. The PHP5 way of doing constructors is with the __construct() function. If you do decide to use a constructor, I'd make sure to call parent::__construct(); in it to ensure the parent AppController class loads correctly.
In looking at the Concatenate() method's functionality, I doubt you intend to have that as your constructor anyway. Rename that function to something clear like saveConcat(). Also, I'm not sure I would be using $this->request->data as your source in case you want to be able to reuse this function and call it with any value. In that case, I'd add a parameter to the function
class Concatenate extends AppModel {
function saveConcat($data) {
if ($this->Concatenate->save($data)) {
$this->Session->setFlash('Your post has been saved.');
$this->redirect(array('action' => 'index'));
} else {
$this->Session->setFlash('Unable to add your post.');
}
}
}
Then somewhere in your controller, you will have to actually call this function. Modify your add() function from your controller to be something like this:
public function add() {
if ($this->request->is('post')) {
// Put data into array for saving
$data[] = array( 'original' => $this->request->data );
$data[] = array( 'concatenated' => $original."m" );
// Call model function to save it
$this->Concatenate->saveConcat($data);
}
}
[EDIT 2]
I just can't figure out why I'm getting the error: Call to a member function save() on a non-object.
When you call $this->Concatenate->save from inside the Concatenate class, that means you are trying to access a variable inside the class called Concatenate and execute a function. Neither of which exist of course. The reason is you need to call the object itself as such:
$this->save("blah blah");
That method (I'm assuming is a parent method from the AppModel class) will be called referencing the current instance of the Concatenate object.

Categories