Displaying default value for attributes that are null

I hope I can be forgiven for asking so many questions recently. I’m new to Yii, and although I’ve read the documentation, including the API, definitive guide, and many of the cookbook topics, I’m not always sure which piece of this sizable (and impressive, I might add) library will best solve the issues I am encountering.

So, I am helping to build this application that displays information about schools, and one of the "problems" is that for each school (represented by a single record in a single table), there are multiple related tables that may or may not have corresponding records in them. Most of these tables are related to the school table in a "has_one/belongs_to" manner.

The issue I’m encountering is that we have fatal errors from PHP when our view file tries to access properties of related models (e.g. echo $school->otherModel->someProperty) that have not been instantiated (because of the lack of a corresponding record).

The hack I came up with for this is extremely messy and goes a little something like this:

  • Rewrite the school controller’s loadModel() method so that after the school::model()->findByPk() method is called, I can

  • loop through each of the items in the school’s relations() array, and

  • if the type of the item is not "object" (resulting from the HAS_ONE relationship) or "array" (from a HAS_MANY relationship),

  • instantiate a new object $item, and

  • Loop through each of the $item’s attributes, and if it is one of the attributes I want to display,

  • assign a default value to it (like "Data not available").

This solution works (sort of), but it is really ugly, and I’d like to find a much cleaner way to ensure that there are no “blank spaces” in my view file resulting from printing out attribute values that are null.

If anyone has any suggestions for a cleaner solution, I am all ears. If you live near me (Florida), I will even buy you a beer! ;)

Thanks!

Tom

p.s. Here is my hack, to satisfy morbid curiosity:




public function loadModel()

{

	if($this->_model===null)

	{

		if(isset($_GET['id']))

			$this->_model=School::model()->with(

			'admission',

			'degree',

			'enrollment',

			'financialaid',

			'graduation',

			'insurers',

			'lenders',

			'testscore',

			'tuition'

		)->findbyPk($_GET['id']);


		if($this->_model===null)

			throw new CHttpException(404,'The requested page does not exist.');

	}


	// Loop through School model's related models, and if no corresponding

	// object was created (i.e. there is no corresponding record in the

	// database), then instantiate an empty instance of each missing model.

	// This is designed to prevent PHP fatal errors when the view

	// attempts to access properties of related models for which there

	// may be no corresponding records in the database.

	foreach(array_keys($this->_model->relations()) as $child)

	{

		$type = gettype($this->_model->$child);

		if ($type != 'object' && $type != 'array')

		{

			$this->_model->$child = new $child; // e.g. if school doesn't have a (whatever), create one;

			

			Yii::trace("Type of \$child is: " . gettype($child));


                        // Loop through $child's attributes and set value to 

                        // application "data not available" string

			if ($child != 'Lender' && $child != 'Insurance')

			{

				foreach($this->_model->$child->getAttributes() as $key=>$value)

				{

					if ($key != 'school_id' && (!stristr($key, 'Period')))

					{

						$this->_model->$child->setAttribute($key, CActiveRecordExtended::NA);

					}

				}

			}

		}

	}


	return $this->_model;

}



Why you need this? Can you explain more?

As you noted, when relation item is not found it either returns null or empty array. So why not check for that?

I guess I could test for a null value or empty array in the view files, but that would result in tons of if/else statements in the view. I’m hoping to reduce the amount of repetitive code I have to place in the views by putting most of the logic in the models (trying to follow RoR’s DRY philosophy).

Also, I extended ActiveRecord with a class I called ActiveRecordExtended so that I could add some methods and attach a behavior to it that I want all of the models to have (all of them are now subclasses of ActiveRecordExtended).

So, with this customization, I now get this error when a view calls a method on an object that doesn’t exist:




Fatal error: Call to a member function formatInt() on a non-object in /home/adirectory/another_directory/someotherdirectory/protected/views/school/view.php on line 51



Maybe I am doing something else wrong? Or have not defined the model relationships correctly?

Hm don’t know about the error.

But maybe a better solution. Use the __get() hack from the other thread and call getDummyRelation($name) if the relation ($name) exists and the relational model is null or empty:




public function getDummyRelation($name)

{


   $model = new $name;


   // Here do your logic (setting default values for specific relation)


   return $model;


}



// Maybe it’s possible to directly set the default values inside of each model class. So you don’t have to do the logic?

Hmm, that sounds like the kind of clean solution I’m looking for.

I’m not sure I get exactly what you’re suggesting, though. Are you saying I could call getDummyRelation($name) from within the SchoolController::loadModel() method? Or somewhere else?

When you try to access the "null" relation like this ("relation" is null)




echo $model->relation->something;



then override __get() of ActiveRecord. In there check if the given $name is a valid relation (in this case “relation” is a valid relation). If that’s the case check if it’s null. If that’s also the case you do




return $this->getDummyRelation($name);



otherwise you just call the parent implementation of __get().

Within the getDummyReleation() method you create the model (with help of $name variable) and may set default values.

Hey, I hope you don’t mind me bugging you with these n00b questions, but I am still getting confused when trying to follow the chain of execution from CComponent::__get() to CActiveRecord::__get().

Here is what I have so far. I am not sure if ActiveRecord::hasRelated() method is the one I should be using here, but since ActiveRecord::_related is private, it seemed like the most logical place to start:




// From my subclass - CActiveRecordExtended

public function __get($name)

{

    $getter='get'.$name;

    if(method_exists($this,$getter))

    {

        return $this->$getter();

    }

    elseif ($this->hasRelated($name))

    {

        if (!is_object($this->$name) && !is_array($this->$name))

        {

            return $this->getDummyRelation($name);

        }

    }


    return parent::__get($name);

}



Right now, all I have defined in CActiveRecordExtended::getDummyRelation() is this:




protected function getDummyRelation($name)

{

    $test = new $name;

    Yii::trace("getDummyRelation() was called.");

    Yii::trace("Instantiated a new " . get_class($test) . " object.");

}



When getDummyRelations($name) is executed, I can see in the trace that the object I am expecting to be instantiated (Enrollment, in this case) is, in fact, being instantiated. However, the trace also indicates that a Tuition object has been instantiated. This made no sense to me, because the School record I am testing does have a related record in the Tuition table. My understanding of the ActiveRecord::hasRelated($name) method is that it returns true if the $name parameter passed to it exists as a key in the private ActiveRecord::_related array. The documentation for that method says that it returns true “if the named related object(s) has been loaded,” but I am not sure if that means the object simply been defined in the model’s relations() method, or if, by “has been loaded,” it means the object both has been defined in the model’s relations() method AND has had record(s) loaded from the database into it (an important difference, since I’m trying to catch objects for which there are no records in the database).

Am I on the right track here? Or way off?

Thanks in advance!

I’m happy to help just ask question :) Also I’m not much deep into that AR stuff as well.

I don’t know if problem is really related to hasRelated(), but you may try this.




...

elseif (array_key_exists($name, $this->relations()))

{

   ...

}



If it doesn’t work we need to take a deeper look.

Thanks, man! Its great to have a little help with problems like this. Sometimes another person can provide a unique perspective.

Well, I definitely believe we’re on the right track with:




elseif (array_key_exists($name, $this->relations()))

{

   ...

}



The relations I referenced in the view are definitely being found in $this->relations() array. However, my next test statement, to check that $this->$name is not an object (e.g. HAS_ONE relation) or an array of objects (HAS_MANY), fails. The value of $this->$name is null. So getDummyRelation is creating new objects regardless of whether the database had records for those objects or not.

I’m not sure if that makes much sense. Here is the code I have at this point:




// From: CActiveRecordExtended extends CActiveRecord

    public function __get($name)

    {

        $getter='get'.$name;

        if(method_exists($this,$getter))

        {

            return $this->$getter();

        }

        elseif (array_key_exists($name, $this->relations()))

        {

            if (!is_object($this->$name) && !is_array($this->$name))

            {

                Yii::trace("I am a " . $name);

                Yii::trace("Result of calling get_class(\$this->\$name): " . get_class($this->$name));

                return $this->getDummyRelation($name);

            }

        }


        return parent::__get($name);

    }

    

    protected function getDummyRelation($name)

    {

        $test = new $name;

        $this->addRelatedRecord($name, $test, FALSE);

        Yii::trace("\$test is a " . get_class($test) . " class.");

        Yii::trace("\$test is a child of " . get_parent_class($test) . ".");

        Yii::trace("\$this->\$name is a " . get_class($this->$name) . " class.");

        Yii::trace("\$this->\$name is a child of " . get_parent_class($this->$name) . ".");

        $structure = print_r($this->$name, TRUE);

        Yii::trace("Structure of \$this->\$name: " . $structure);

        $is_ar_instance = ($test instanceof CActiveRecord) ? 'yes' : 'no';

        Yii::trace("Is \$test an instance of CActiveRecord? {$is_ar_instance}");

    }



I think this is the issue - in CActiveRecordExtended::__get(), when I test "if (!is_object($this->$name && !is_array($this->$name)) it is failing because:

  1. Even if $name is one of the relations $this has, it is not an object (at this point in the execution) because the PHP interpreter has not yet reached the code in CActiveRecord which would create it, and

  2. it is also not an array, for the same reason as in (1) and because the items I am testing are associated with the School with a has_one/belongs to relationship, not a many_many.

So I am kind of at a loss here, unable (yet) to figure out how to get $name into the School’s related objects. I thought for sure addRelatedRecord() would do it, but it doesn’t seem to be working.

Any ideas?

I see. What about this?




...


$relation = $this->getRelated($name);


if (!is_object($relation) && !is_array($relation))

...



Great minds must think alike, my friend, because that is pretty similar to what I came up with. ;)

Here is what I have now, which seems to be working:




    public function __get($name)

    {

        $getter='get'.$name;

        if(method_exists($this,$getter))

        {

            return $this->$getter();

        }


        if (array_key_exists($name, $this->relations()))

        {

            $exists = $this->getRelated($name);

            if (!is_object($exists) || $exists === NULL)

            {

                return $this->getDummyRelation($name);

            }

        }


        return parent::__get($name);

    }

    

    protected function getDummyRelation($name)

    {

        $test = new $name; // Create a new whatever.

        $this->addRelatedRecord($name, $test, FALSE); // Add the new whatever to parent objects relations() array.

        $gotRelated = $this->getRelated($name);

        $gotRelated_Structure = print_r($gotRelated, TRUE);

        Yii::trace("Structure of {$gotRelated_Structure}"); // Prints structure of newly created object. Woohoo!

    }



At this point, I think all I need to do is override the CActiveRecord::init() method in each of my models and assign default values to the attributes. I am hoping that will work. I’ll post the results…

Okay nice. Can you also test if each relation gets only loaded (checked) once? Because I don’t know at this point how getRelated() works internally - possible that it loads the relation more than one time maybe.

For the default values. Maybe you can try to just add those to the class:




public $something = "not available";



Though, wouldn’t be very dynamic.

Yes, good call - I definitely need to test that (although I will do it tomorrow; it’s time to go home now).

I handled setting the default values this way:




    public function init()

    {

        parent::init();

        

        foreach($this->getAttributes() as $attribute => $value)

        {

            if ($attribute != 'school_id')

            {

                if (!isset($value) || $value === NULL || $value == '')

                {

                    $this->$attribute = CActiveRecordExtended::NA;

                }

            }

        }


        $testAttributes = print_r($this->getAttributes(), TRUE);

        Yii::trace("Result of assigning default attribute values in Enrollment::init(): {$testAttributes}");

    }



There is still some weirdness going on with one of the attributes of the Enrollment model. I can see by printing print_r($attribute) in Yii::trace that this particular attribute is being assigned the default value, just like the rest of them. And I can call $school->enrollment->totalMale or any of the other ones in the view, and Yii prints out my default value. But when I ask Yii to print $school->enrollment->total in the view, nothing is printed.

Scratch that last part. It looks like the first time I try to access one of the enrollment attributes, nothing is printed. Then each subsequent time I try to access one of them, everything works fine.

This leads me to think maybe my code above is not doing what I expect and that it is the CActiveRecord::__get() method (not my subclass method) that is creating the object.

And the confusion grows… :blink:

Ok, I am reasonably confident that I’ve solved that issue now. I was (d’oh!) not returning the newly created object after intantiating and initializing it with the default values. This meant that the new object wasn’t being added to the School’s relations array until the second time it was referenced.

Here is the code I have now. It still needs to be refactored, but at least it is working:




// From CActiveRecordExtended:


    public function __get($name)

    {

        // Have we defined a getter method for this attribute? If so, return that.

        $getter='get'.$name;

        if(method_exists($this,$getter))

        {

            return $this->$getter();

        }


        // If $name is not associated with a getter method, does it match one of the

        // this object's related models?

        if (array_key_exists($name, $this->relations()))

        {

            // So, $name does match with the name of a related model. Now, is there an instance

            // of this related model (i.e., does the database have a corresponding record?

            $exists = $this->getRelated($name);

            if (!is_object($exists) || $exists === NULL)

            {

                // There is no corresponding record in the database for this model.

                // getDummyRelation will create a new instance for us. The model will

                // then run its init() method, which sets default values for all of its attributes.

                $this->getDummyRelation($name);


                // Now, return the newly created and initialized object.

                return $this->getRelated($name);

            }

        }


        return parent::__get($name);

    }

    

    protected function getDummyRelation($name)

    {

        $whatever = new $name; // Create a new whatever.

        $this->addRelatedRecord($name, $whatever, FALSE); // Add the new whatever to parent objects relations() array.

    }