![]() |
|
Snippets |
|
I needed to have many subclasses inherited from a main class.
To do that with propel, you need to have all your classes in one big table. I wanted to have separate tables.
Then I discovered the propel behaviors and it does that very well !
Let's see an example with a master class "element" and a subclass "subElement" :
The tables of the subclasses just need to have an "element_id" foreign key column, to be linked with the "element" master class table :
# schema.yml example propel: # master class element: _attributes: { phpName: Element } id: name: varchar(255) content: longvarchar created_at: # subclass with only the subclass properties and element_id foreign key column sub_element: _attributes: { phpName: SubElement } element_id: author: varchar(255)
Activate the behaviors in "project/config/propel.ini" (before building the model!):
propel.builder.AddBehaviors = true
Don't forget to rebuild the model, anytime you modify it :
>symfony propel-build-model
Create your behavior in "project/config/config.php" :
// get propel behavior lib require_once($sf_symfony_lib_dir.'/addon/propel/sfPropelBehavior.class.php'); // declare all the methods from of the master class "element", that you'll need in the subclasses sfPropelBehavior::registerMethods('ElementBehavior', array( array('ElementBehavior', 'setName'), array('ElementBehavior', 'getName'), array('ElementBehavior', 'setContent'), array('ElementBehavior', 'getContent'), array('ElementBehavior', 'setCreated'), array('ElementBehavior', 'getCreatedAt'), ) ); // Modify save() and delete() master class methods // SubElement->save() need to run Element->save()..., and so for SubElement->delete() sfPropelBehavior::registerHooks('ElementBehavior', array( ':save:pre' => array('ElementBehavior', 'preSave'), ':delete:pre' => array('ElementBehavior', 'preDelete'), ) );
Declare all these behavior methods in the master class model (here in "project/lib/model/Element.php") :
// declare this class after the element class class ElementBehavior { /* Properties Setters */ public function setName(BaseObject $object, $value) { if (!$object->getElement()) $object->setElement(new Element()); $object->getElement()->setName($value); } public function setContent(BaseObject $object, $value) { if (!$object->getElement()) $object->setElement(new Element()); $object->getElement()->setContent(value); } public function setCreatedAt(BaseObject $object, $value) { if (!$object->getElement()) $object->setElement(new Element()); $object->getElement()->setCreatedAt($value); } /* Properties Getters */ public function getId(BaseObject $object) { if (!$object->getElement()) $object->setElement(new Element()); return $object->getElement()->getId(); } public function getName(BaseObject $object) { if (!$object->getElement()) $object->setElement(new Element()); return $object->getElement()->getName(); } public function getContent(BaseObject $object) { if (!$object->getElement()) $object->setElement(new Element()); return $object->getElement()->getContent(); } public function getCreatedAt(BaseObject $object) { if (!$object->getElement()) $object->setElement(new Element()); return $object->getElement()->getCreatedAt(); } /* Element save method called just before SubElement save */ public function preSave(BaseObject $object) { // if element not null... if ($object->getElement()) { // save element in his table $object->getElement()->save(); // if new element, capture id if ($object->isNew()) $object->setElementId($object->getElement()->getId()); } } /* Element delete method called just before SubElement delete */ public function preDelete(BaseObject $object) { // if element not null and not new... if ($object->getElement() && !$object->getElement()->isNew()) { // delete element in his table $object->getElement()->delete(); } } }
add the "ElemenBehavior" to all your subclasses (here in" project/lib/model/SubElement.php") :
sfPropelBehavior::add('SubElement', array('ElementBehavior'));
That's all ! You can test it with a code like this :
// create subElement $test = new SubElement(); // test on element methods $test->setName('test'); $test->setContent('this is the big test !'); // test also SubElement methods $test->setAuthor('mr john smith'); // save in element table AND in sub_element table ! $test->save(); // capture subElement with id 1 $test = SubElementPeer :: retrieveByPk(1); // delete element and subElement in the two tables $test->delete(); // mix criteria for both class $c = new Criteria(); $c->add(ElementPeer :: ID, 1); $c->add(SubElementPeer :: AUTHOR, 'mr john smith'); // get a list of the subElements $subElements = SubElementPeer :: doSelectJoinElement(new Criteria());
each time you add a column or a method to the element model, you need to declare it in the behavior to be handled in the subclasses.
It's a way to go, may be there is a simplier way, but this works well for me...
Say that you have a model object Son that inherits from the model object Father, via propel inheritance. You want to create an admin generator interface for the object Son. The problem is that propel does not generate a SonPeer class, so you'll have to call:
symfony propel-init-admin Father
but the list function will list all the Father objects instead of the Son objects only. Same problem with the create function that will create a Father object, not a Son object.
Here is a solution. You will have to overload getFatherOrCreate and addFiltersCriteria:
// add this in the actions.class.php of your admin module protected function addFiltersCriteria(&$c) { $c->add(FatherPeer::CLASS_KEY, FatherPeer::CLASSKEY_SON); parent::addFiltersCriteria($c); } protected function getFatherOrCreate ($id = 'id') { $son = parent::getFatherOrCreate($id); if ($son->isNew()) // if it is a new one then we create a Son object $son = new Son(); else $this->redirect404Unless($son->isSon()); // we check that we really got a son object return $son; }
Now everything will work as if you were using the Son object.
When you use class inheritance in Propel, it can be quite useful to get a sorted array of all the descendant classes of any base class in your model.
First we need a simple method to determine whether a particular class inherits from a base class. We do this by ascending the class inheritance tree until we either a) find the base class we're looking for or b) reach the top of the tree
function inheritsFrom($AClass,$ABaseClass) { $bIsSubClass=false; $strClass=$AClass; while (($strClass!='') and !$bIsSubClass) { $bIsSubClass=(get_parent_class($strClass)==$ABaseClass); $strClass=get_parent_class($strClass); } return $bIsSubClass; }
Then we can extend the sfCore class, as shown below, to generate an array of sub-classes, that exist in the "model". I've added a couple of extra options, to allow class names to be converted to class key constants, and abstract classes to be ignored, if desired.
class EnhancedCore extends sfCore { public static function subClassesOf($AClass,$AKeyField='',$AIgnoreAbstractClasses=false) { $modelDir=sfConfig::get('sf_model_lib_dir'); $intPathLength=strlen($modelDir); $arrClasses=array(); foreach(self::$classes as $strClass => $strPath) { //ignore classes outside the "model" library folder if (substr($strPath,0,$intPathLength)==$modelDir) { if (inheritsFrom($strClass,$AClass)) { $bIgnoreClass=false; if ($AIgnoreAbstractClasses) { $reflector=new ReflectionClass($strClass); $bIgnoreClass=$reflector->isAbstract(); } if (!$bIgnoreClass) { $strKey=$strClass; if ($AKeyField!='') { $peerReflector=new ReflectionClass($AClass.'Peer'); $strConstant=strtoupper($AKeyField).'_'.strtoupper($strClass); if (array_key_exists($strConstant,$peerReflector->getConstants())) { $strKey=$peerReflector->getConstant($strConstant); } } $arrClasses[$strKey]=$strClass; } } } } asort($arrClasses); return $arrClasses; } }
So how about a simple example, in order to demonstrate its use. Let's say we have a schema.xml file like so:
<table name="person" <column name="id" type="INTEGER" required="true" autoIncrement="true" primaryKey="true" /> <column name="name" type="VARCHAR" size="50" required="true"/> <column name="classkey" type="INTEGER" required="true" inheritance="single"> <inheritance key="1" class="Employee" extends="person"/> <inheritance key="2" class="Customer" extends="person"/> <inheritance key="3" class="Manager" extends="Employee"/> </column> </table>
Symfony produces one base class (Person) and one peer class (PersonPeer) from this schema, as well as three sub-classes (Employee,Customer,Manager). Then the following simple code
$classes=EnhancedCore::subClassesOf('Person','classkey'); var_dump($classes)
outputs >>>
array(3) { [2]=> string(8) "Customer" [1]=> string(8) "Employee" [3]=> string(7) "Manager" }
I needed to have many subclasses inherited from a main class.
To do that with propel, you need to have all your classes in one big table. I wanted to have separate tables.
Then I discovered the propel behaviors and it does that very well !
Let's see an example with a master class "element" end a subclass "subElement" :
The tables of the subclasses just need to have an "element_id" foreign key column, to be linked with the "element" master class table :
# schema.yml example propel: # master class element: _attributes: { phpName: Element } id: name: varchar(255) content: longvarchar created_at: # subclass with only the subclass properties and element_id foreign key column sub_element: _attributes: { phpName: SubElement } element_id: author: varchar(255)
Activate the behaviors in "project/config/propel.ini" :
propel.builder.AddBehaviors = trueCreate your behavior in "project/config/config.php" :
// get propel behavior lib require_once($sf_symfony_lib_dir.'/addon/propel/sfPropelBehavior.class.php'); // declare all the methods from of the master class "element", that you'll need in the subclasses sfPropelBehavior::registerMethods('ElementBehavior', array( array('ElementBehavior', 'setName'), array('ElementBehavior', 'getName'), array('ElementBehavior', 'setContent'), array('ElementBehavior', 'getContent'), array('ElementBehavior', 'setCreated()'), array('ElementBehavior', 'getCreatedAt()'), ) ); // Modify save() and delete() master class methods // SubElement->save() need to run Element->save()..., and so for SubElement->delete() sfPropelBehavior::registerHooks('ElementBehavior', array( ':save:pre' => array('ElementBehavior', 'preSave'), ':delete:pre' => array('ElementBehavior', 'preDelete'), ) );
Declare all these behavior methods in the master class model (here in "project/lib/model/Element.php") :
// declare this class after the element class class ElementBehavior { /* Properties Setters */ public function setName(BaseObject $object, $value) { if (!$object->getElement()) $object->setElement(new Element()); $object->getElement()->setName($value); } public function setContent(BaseObject $object, $value) { if (!$object->getElement()) $object->setElement(new Element()); $object->getElement()->setContent(value); } public function setCreatedAt(BaseObject $object, $value) { if (!$object->getElement()) $object->setElement(new Element()); $object->getElement()->setDescription($value); } /* Properties Getters */ public function getId(BaseObject $object) { if (!$object->getElement()) $object->setElement(new Element()); return $object->getElement()->getId(); } public function getName(BaseObject $object) { if (!$object->getElement()) $object->setElement(new Element()); return $object->getElement()->getName(); } public function getContent(BaseObject $object) { if (!$object->getElement()) $object->setElement(new Element()); return $object->getElement()->getContent(); } public function getCreatedAt(BaseObject $object) { if (!$object->getElement()) $object->setElement(new Element()); return $object->getElement()->getCreatedAt(); } /* Element save method called just before SubElement save */ public function preSave(BaseObject $object) { // if element not null... if ($object->getElement()) { // save element in his table $object->getElement()->save(); // if new element, capture id if ($object->isNew()) $object->setElementId($object->getElement()->getId()); } } /* Element delete method called just before SubElement delete */ public function preDelete(BaseObject $object) { // if element not null and not new... if ($object->getElement() && !$object->getElement()->isNew()) { // delete element in his table $object->getElement()->delete(); } } }
add the "ElemenBehavior" to all your subclasses (here in" project/lib/model/SubElement.php") :
sfPropelBehavior::add('SubElement', array('ElementBehavior'));
That's all ! You can test it with a code like this :
// create subElement $test = new SubElement(); // test on element methods $test->setName('test'); $test->setContent('this is the big test !'); // test also SubElement methods $test->setAuthor('mr john smith'); // save in element table AND in sub_element table ! $test->save(); // capture subElement with id 1 $test = SubElementPeer :: retrieveByPk(1); // delete element and subElement in the two tables $test->delete(); // mix criteria for both class $c = new Criteria(); $c->add(ElementPeer :: ID, 1); $c->add(SubElementPeer :: AUTHOR, 'mr john smith'); // get a list of the subElements $subElements = SubElementPeer :: doSelectJoinElement(new Criteria());
each time you add a column or a method to the element model, you need to declare it in the behavior to be handled by the subclasses.
It's a way to go, may be there is a simplier way, but this works well for me...