Extending Yii model with predefined value - php

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.

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

Yii Framework - How do I get a different value for a field?

I have a table with a field called vat_free. So my model was created with a property $vat_free. Its value can be 0 or 1.
I want my view to show No or Yes, instead of 0 or 1. I can do it creating a getter like getVatFree(), but it seems like a messy solution, because then I'll have two properties to the same field, even though it would serve different purposes.
So how can I use only the original property $vat_free? Couldn't I modify its getter?
Creating method
public function getVatFreeString(){
return $this->vat_free ? 'Yes':'No';
}
Is proper solution, it's not messy.
You could do like
$vat_free = YES or NO
but right before save this object you would override object class with beforeSave() method like following:
beforeSave(){
if($this->vat_free = YES){
$this->vat_free = 1
}else{
$this->vat_free = 0;
}
}
and override afterFind() to do the reverse(for beforeSave()) translate. But this is even messy and will not work if u do bulk save or retrieve.
I see 2 solutions.
Go with what you have said getVatFree(), this is whole purpose of OOP encapsulation.
Instead of making 1 or 0 in db, do Y or N values, you can use them in both places without problems.
In your model, create a new field that will be used for display purposes only.
class User extends CActiveRecord
{
public $displayVatFreeFlag;
public function rules() { ... }
public function afterFind()
{
$this->displayVatFreeFlag = ($this->vat_free ? 'Yes':'No');
}
}
Then, in your field, display the field as normal.
Vat free : <?php echo $model->displayVatFreeFlag; ?>

Yii on update, detect if a specific AR property has been changed on beforeSave()

I am raising a Yii event on beforeSave of the model, which should only be fired if a specific property of the model is changed.
The only way I can think of how to do this at the moment is by creating a new AR object and querying the DB for the old model using the current PK, but this is not very well optimized.
Here's what I have right now (note that my table doesn't have a PK, that's why I query by all attributes, besides the one I am comparing against - hence the unset function):
public function beforeSave()
{
if(!$this->isNewRecord){ // only when a record is modified
$newAttributes = $this->attributes;
unset($newAttributes['level']);
$oldModel = self::model()->findByAttributes($newAttributes);
if($oldModel->level != $this->level)
// Raising event here
}
return parent::beforeSave();
}
Is there a better approach? Maybe storing the old properties in a new local property in afterFind()?
You need to store the old attributes in a local property in the AR class so that you can compare the current attributes to those old ones at any time.
Step 1. Add a new property to the AR class:
// Stores old attributes on afterFind() so we can compare
// against them before/after save
protected $oldAttributes;
Step 2. Override Yii's afterFind() and store the original attributes immediately after they are retrieved.
public function afterFind(){
$this->oldAttributes = $this->attributes;
return parent::afterFind();
}
Step 3. Compare the old and new attributes in beforeSave/afterSave or anywhere else you like inside the AR class. In the example below we are checking if the property called 'level' is changed.
public function beforeSave()
{
if(isset($this->oldAttributes['level']) && $this->level != $this->oldAttributes['level']){
// The attribute is changed. Do something here...
}
return parent::beforeSave();
}
Just in one line
$changedArray = array_diff_assoc($this->attributes,
$this->oldAttributes);
foreach($changedArray as $key => $value){
//What ever you want
//For attribute use $key
//For value use $value
}
In your case you want to use if($key=='level') inside of foreach
Yii 1.1: mod-active-record at yiiframework.com
or Yii Active Record instance with "ifModified then ..." logic and dependencies clearing at gist.github.com
You can store old properties with hidden fields inside update form instead of loading model again.

Design pattern for repetitive switch in getters?

I've an ORM model (PHP Active Record), say, for a blogging system. I've something that's a post model that stores the number of likes. The post could either be a picture or quote (say), and they are different tables (and hence models).
The schema is that a post holds data like number of shares, likes, description, etc. along with either a picture or a quote.
So when writing getters for the post model I'm having to write
public function getX() {
if ($this->isPicture()) {
return $this->picture->getX();
}
else if ($this->isQuote()) {
return $this->quote->getX()
}
else {
return self::DEFAULT_X
}
}
I'm currently having to write this structure for many getter. Is there something I can do to avoid that?
PS: Tagged as PHP because that's my code in.
EDIT
Changed comments to code.
This is a model (and a corresponding table in the DB) that has more data than just a picture and quote. Example, description that's part of the post and doesn't reside on either the picture or the quote.
There's tables for pictures and quotes.
Using PHP Active Record and each of the three classes extends the generic model class provided by PHP Active Record.
The picture model has it's own data. Same for quote.
To expand on the idea of the Strategy pattern mentioned in the comments:
class Post {
// get the correct 'strategy'
public function getModel() {
if ($this->isPicture()) {
return $this->picture;
}
if ($this->isQuote()) {
return $this->quote;
}
return null;
}
// using the strategy
public function getX() {
$model = $this->getModel();
if (null === $model) {
return self::DEFAULT_X;
}
return $model->getX();
}
}
Each strategy would presumably implement the same interface as the Post class for exposing those getters. Even better would be to provide a default strategy (rather than returning null) and have that return the default values. That way, the null check in each getter becomes redundant.
An alternative approach to this is a very basic form of metaprogramming. The idea is that you go a level higher than calling your methods by hand, and let the code do it for you.
(Assume that the method definitions are all part of Post)
public function getX($model = null) {
if ($model) return $model->getX();
else return self::DEFAULT_X;
}
// usage
$postModel->getX($pictureModel);
What's happening here is that, in this single instance of getX in your Post model, you're passing in the name of another class, and executing the `getX' method on that instance (if it exists and is callable).
You can extend this in other ways. For example, maybe you don't want to pass an instance in, when the method can do it anyway:
public function getX($model_name = null) {
if ($model_name && $class_exists($model_name) && is_callable(array($model_name, 'getX')) {
$model = new $model_name;
return $model->getX();
} else {
return self::DEFAULT_X;
}
}
// usage
$postModel->getX('Picture');
In this instance, you pass the model in as a string, and the method will do the rest. While this makes it quicker to get what you want, you might find that you don't want to work with fresh instances all the time (or you can't), so there's a bit of a trade-off with this 'convenient' way.
That still doesn't fully solve your problem, though, since you still have to repeat that for each getter, over and over again. Instead, you can try something like this:
public function __call($method, $args) {
$class = $args[0];
if (class_exists($class) && is_callable(array($class, $method))) {
$model = new $class;
return $model->$method();
}
}
// usage
$postModel->getX('Picture');
$postModel->getY('Quote');
$postModel->getZ('Picture');
If you call a function that doesn't exist on the Post model, that magic method will be called, and it'll fire up a new instance of the model name you supply as an argument, and call the getWhatever method on it, if it exists.
It's important to note that you must not define these getters in Post, unless you want to override the methods in the other classes.
There is still the problem of this creating new instances all the time, though, and to remedy this you can use a bit of dependency injection. This means that you let the Post class contains a list of other instances of classes that it wants to use in future, so you can add and remove them at will.
This is what I would consider the actual solution, with the other examples hopefully showing how I've got here (will edit to clarify things, of course).
public $models = array();
public function addModel($instance) {
$this->models[get_class($instance)] = $instance;
}
public function __call($method, $args) {
$class = $args[0];
if (array_key_exists($class, $this->models)) {
$model = $this->models[$class];
if (is_callable(array($model, $method)) {
return $model->$method();
}
}
}
// usage
$this->addModel($pictureModel);
$this->addModel($quoteModel);
$this->getX('Picture');
$this->getY('Quote');
Here, you're passing in your existing instances of models into the Post class, which then stores them in an array, keyed by the name of the class. Then, when you use the class as described in the last example, instead of creating a new instance, it will use the instance it has already stored. The benefit of this is that you might do things to your instances that you'd want reflected in the Post model.
This means that you can add as many new models as you like that need to plug into Post, and the only thing you need to do is inject them with addModel, and implement the getters on those models.
They all require you to tell the class what models to call at some point or another. Since you have an array of dependent models, why not add a way to get everything?
public function __call($method, $args) {
$class = $args[0];
if (array_key_exists($class, $this->models)) {
$model = $this->models[$class];
if (is_callable(array($model, $method)) {
return $model->$method();
}
} elseif ($class === 'all') {
// return an array containing the results of each method call on each model
return array_map(function($model) use ($method) {
if (is_callable(array($model, $method) return $model->$method();
}, $this->models);
}
}
// usage
$postModel->getX('all');
Using this, you'll get an array containing the return values of each getX method on each model you added with addModel. You can create pretty powerful functions and classes that do all this stuff without you having to repeat tedious logic.
I have to mention that these examples are untested, but at the very least I hope the concept of what you can do has been made clear.
Note:
The same thing can be applied to __GET and __SET methods, too, which are used for accessing properties. It's also worth saying that there may be the slight risk of a library already using these magic methods, in which case you'll need to make the code a little more intelligent.

PropertyObject Class Help

I am new to php and currently I am reading Wrox Professional PHP 5.
Can anyone explain me the following code ?
<? php
abstract class PropertyObject
{
//Stores name/value pairs that hook properties to database field names
protected $propertyTable=array();
//List of properties that have been modified.
protected $changedProperties=array();
//Actual data from the database.
protected $data;
//Any validation errors that might have occured.
protected $errors=array();
public function __construct($arData)
{
$this->data=$arData;
}
function __get($propertyName)
{
if(!array_key_exits($propertyName,$this->propertyTable))
{
throw new Exception("Invalid property \"$propertyName\" !");
}
if(method_exists($this,'get'.$propertyName))
{
return call_user_func(array($this,'get'.$propertyName));
}
else
{
return $this->data[$this->propertyTable[$propertyName]];
}
}
function __set($propertyName,$value)
{
if(!array_key_exits($propertyName,$this->propertyTable))
{
throw new Exception("Invalid property \"$propertyName\" !")
}
if(method_exits($this,'set'.$propertyName))
{
return call_user_func(array($this,'set'.$propertyName),$value);
}
else
{
//If the value of the property really has changed and it's not already in the changedProperties array, add it.
if($this->propertyTable[$propertyName] !=$value && !in_array($propertyName,$this->changedProperties))
{
$this->changedProperties[]=$propertyName;
}
//Now set the new value
$this->data[$this->propertyTable[$propertyName]]=$value;
}
}
}
?>
I can't understand the code inside assessor get and set methods.
The __get magic method is called when a property of the object is requested but it wasn't declared or specifically assigned (for dynamic properties). This implementation:
First tries to see if the logical property exists as an entry in the actual declared property named $propertyTable.
If it doesn't exist, it throws an exception, therefore leaving the method,
If it exists and additionaly exists a method named 'get'.$propertyName (i.e., "get" concatenated with the request property name), that method is called and its value is returned.
If it exists but there's no such method, it returns the value of the entry with key $propertyName in the declared property $propertyTable.
Given this, I think you can figure __set out. See Magic Methods in the PHP manual.
This is a really common way of setting up a DB storage class. What happens is you instantiate an object based on PropertyObject (as PropertyObject is abstract)
class MyObj extends PropertyObject {
}
$m = new MyObj();
Which inherits the __get() and __set() methods. Any time the object's data is accessed via the -> operator, the __get() and __set() methods are called, respectively.
$m->foo; #calls MyObject::__get('foo');
$m->bar = 'baz'; #calls MyObject::__set('bar','baz');
The __get() method first checks to see if the there is a key defined in the property table (which here models fields from the DB), and if one does not exist, throws an exception.
Then, get() will see if there is a function defined with the word 'get' prepended. So, assuming foo was a key in the propertyTable, __get() would see if we had defined a method getfoo, and if we had, call it for us, and return its value.
//if(method_exists($this,'get'.$propertyName))
//{
// return call_user_func(array($this,'get'.$propertyName));
//}
$m->foo; # checks if MyObj::getfoo is defined, and if so, calls it
Lastly, if there is a key foo in the propertyTable but no method named getfoo, it would simply return the value of the array position in $m->data whose key is the value of the array position in propertyTable whose key is foo
__set() is defined much the same way, but rather than returning the value stored in the data array instead checks for a prepended 'set', and checks to see if the value being set on the object is any different from the value in the data array, and if it is, adds the property name to the changedProperties array before setting the new value.

Categories