Maybe I should explain how some of this actually works.
The class responsible for cascading the relationship tree is called ActiveRecordCascade. ActiveRecordCascade may take one parameter upon being constructed that is an instance of the IActiveRecordCascadeAction interface. ActiveRecordDelete (show below) implements IActiveRecordCascadeAction. The interface method of iActiveRecordCascadeAction is called on every node within the relationship hierarchy. If the doSomething() method of the IActiveRecordCascadeAction return false then the node becomes a leaf element and its children are omitted. Therefore, by comparing the parent to the current node inside the doSomething method it is possible to remove a entire branch. It is also possible to skip a node by just returning true and not running the methods to build the SQL in the ActiveRecordDelete class. What I am looking for is a simple API to control this from outside these classes.
PHP Code:
class ActiveRecordCascade {
protected $action;
public function __construct(IActiveRecordCascadeAction $action) {
$this->action = $action;
}
public function cascade(ActiveRecordCascadeNode $node,$nodes = null) {
if($this->action->doSomething($node,$nodes)===true) {
$this->_resolveHasOne($node,$nodes);
$this->_resolveHasMany($node,$nodes);
$this->_resolveBelongsToAndHasMany($node,$nodes);
}
}
protected function _resolveHasOne(ActiveRecordCascadeNode $node,$nodes = null) {
if($node->getConfig()->hasOne()===true) {
foreach($node->getConfig()->getHasOne() as $model) {
$class = Inflector::classify($model);
$relatedConfig = ActiveRecordModelConfig::getModelConfig($class);
$relatedNode = new ActiveRecordCascadeNode($relatedConfig);
$this->_collectRecord($node,$relatedNode,$model);
$relatedNodes = is_null($nodes)?array():$nodes;
array_unshift($relatedNodes,$node);
$this->cascade($relatedNode,$relatedNodes);
}
}
}
protected function _resolveHasMany(ActiveRecordCascadeNode $node,$nodes = null) {
if($node->getConfig()->hasMany()===true) {
foreach($node->getConfig()->getHasMany() as $model) {
$class = Inflector::classify($model);
$relatedConfig = ActiveRecordModelConfig::getModelConfig($class);
$relatedNode = new ActiveRecordCascadeNode($relatedConfig);
$this->_collectRecords($node,$relatedNode,$model);
$relatedNodes = is_null($nodes)?array():$nodes;
array_unshift($relatedNodes,$node);
$this->cascade($relatedNode,$relatedNodes);
}
}
}
protected function _resolveBelongsToAndHasMany(ActiveRecordCascadeNode $node,$nodes = null) {
if($node->getConfig()->hasBelongsToAndHasMany()===true) {
foreach($node->getConfig()->getBelongsToAndHasMany() as $index=>$reference) {
$class = Inflector::classify($reference[1]);
$relatedConfig = ActiveRecordModelConfig::getModelConfig($class);
$relatedNode = new ActiveRecordCascadeNode($relatedConfig);
$this->_collectRecords($node,$relatedNode,$reference[1]);
$relatedNodes = is_null($nodes)?array():$nodes;
array_unshift($relatedNodes,$node);
$this->cascade($relatedNode,$relatedNodes);
}
}
}
protected function _collectRecords(ActiveRecordCascadeNode $node,ActiveRecordCascadeNode $relatedNode,$property) {
if($node->hasRecords()===true) {
foreach($node->getRecords() as $record) {
if($record->hasProperty($property)===true) {
foreach($record->getProperty($property) as $relatedRecord) {
$relatedNode->addRecord($relatedRecord);
}
}
}
}
}
protected function _collectRecord(ActiveRecordCascadeNode $node,ActiveRecordCascadeNode $relatedNode,$property) {
if($node->hasRecords()===true) {
foreach($node->getRecords() as $record) {
if($record->hasProperty($property)===true) {
$relatedNode->addRecord($record->getProperty($property));
}
}
}
}
}
?>
PHP Code:
<?php
interface IActiveRecordCascadeAction {
public function doSomething(ActiveRecordCascadeNode $node,$nodes=null); // bool
}
?>
PHP Code:
class ActiveRecordDelete implements IActiveRecordCascadeAction {
protected $queries;
public function __construct() {
$this->queries = array();
}
public function getQueries() {
return $this->queries;
}
public function query(PDO $db) {
$total = count($this->queries);
if($total==0) return;
for($i=($total-1);$i>=0;$i--) {
foreach($this->queries[$i] as $query) {
try {
echo '<p>',$query->getSql(),'</p>';
echo '<pre>',print_r($query->getData()),'</pre>';
//$query->query($db);
} catch(Exception $e) {
return false;
}
}
}
return true;
}
public function doSomething(
ActiveRecordCascadeNode $node
,$nodes=null
) {
$nodes = is_null($nodes)?array():$nodes;
$countNodes = count($nodes);
$query = new ActiveRecordQuery();
if($countNodes>1) {
$field = $node->getConfig()->getRelatedField($nodes[0]->getConfig());
if(empty($field)) {
throw new Exception('Unable to resolve relationship between '.$node->getConfig()->getClassName().' and '.$nodes[0]->getConfig()->getClassName().' inside '.__CLASS__.' class method '.__METHOD__ .' line '.__LINE__.'.');
return false;
}
$subquery = $this->_makeSubquery($nodes,$query);
$sql = 'DELETE FROM `'.$node->getConfig()->getTable().'` WHERE `'.$field.'` IN ('.$subquery.')';
} else if($countNodes==1) {
$field = $node->getConfig()->getRelatedField($nodes[0]->getConfig());
if(empty($field)) {
throw new Exception('Unable to resolve relationship between '.$node->getConfig()->getClassName().' and '.$nodes[0]->getConfig()->getClassName().' inside '.__CLASS__.' class method '.__METHOD__.' line '.__LINE__.'.');
return false;
}
$sql = 'DELETE FROM `'.$node->getConfig()->getTable().'` '.$this->_makeWhereClause(array($node,$nodes[0]),$query,false,$field);
} else {
$sql = 'DELETE FROM `'.$node->getConfig()->getTable().'` '.$this->_makeWhereClause(array($node),$query,false);
}
$query->setSql($sql);
if(array_key_exists($countNodes,$this->queries)) {
$this->queries[$countNodes][] = $query;
} else {
$this->queries[$countNodes] = array($query);
}
return true;
}
protected function _makeSubQuery($nodes,ActiveRecordQuery $query) {
$str = '';
$p=null;
foreach($nodes as $key=>$c) {
if(!is_null($p)) {
$str.= ' INNER JOIN `'.$c->getConfig()->getTable().'` AS t'.$key.' ON t'.($key-1).'.`'.$p->getConfig()->getRelatedField($c->getConfig()).'` = t'.$key.'.`'.$c->getConfig()->getRelatedField($p->getConfig()).'`';
$p = $c;
} else {
$str.= 'SELECT DISTINCT t'.$key.'.`'.$c->getConfig()->getPrimaryKey().'` FROM `'.$c->getConfig()->getTable().'` AS t'.$key;
$p = $c;
}
}
return $str.$this->_makeWhereClause($nodes,$query);
}
protected function _makeWhereClause($nodes,ActiveRecordQuery $query,$subquery=true,$field=null) {
$countNodes = (count($nodes)-1);
$filters = array();
for($i=$countNodes;$i>=0;$i--) {
if($nodes[$i]->hasRecords()===true) {
$placeholders = array();
$primaryKey = $nodes[$i]->getConfig()->getPrimaryKey();
foreach($nodes[$i]->getRecords() as $record) {
$query->addData($record->getProperty($primaryKey));
$placeholders[] = '?';
}
if(!empty($placeholders)) {
$field = !is_null($field) && $i==$countNodes?$field:$primaryKey;
if($subquery===true) {
if(count($placeholders)==1) {
$filters[] = '(t'.$i.'.`'.$field.'` = ?)';
} else {
$filters[] = '(t'.$i.'.`'.$field.'` IN ('.implode(',',$placeholders).'))';
}
} else {
if(count($placeholders)==1) {
$filters[] = '(`'.$field.'` = ?)';
} else {
$filters[] = '(`'.$field.'` IN ('.implode(',',$placeholders).'))';
}
}
}
}
}
return empty($filters)?'':' WHERE '.implode(' AND ',$filters);
}
}
?>
So now the actual code that would go inside the ActiveRecord delete method would be the following:
PHP Code:
public function delete() {
$config = ActiveRecordModelConfig::getModelConfig(get_class($this));
$node = new ActiveRecordCascadeNode($config);
$node->addRecord($this);
try {
$delete = new ActiveRecordDelete();
$cascade = new ActiveRecordCascade($delete);
$cascade->cascade($node);
} catch(Exception $e) {
throw new Exception('Error initializing delete. Exception caught and rethrown from line '.__LINE__.' in class '.__CLASS__.' inside method '.__METHOD__.': '.$e->getMessage());
return false;
}
try {
if($delete->query(self::getConnection())===true) {
$unset = new ActiveRecordDeactivate();
$cascade = new ActiveRecordCascade($unset);
$cascade->cascade($node);
return true;
} else {
return false;
}
} catch(Exception $e) {
throw new Exception('Error executing delete queries. Exception caught and rethrown from line '.__LINE__.' in class '.__CLASS__.' inside method '.__METHOD__.': '.$e->getMessage());
return false;
}
}
Currently there isn't a way to control what will get deleted within the tree hierarchy. Every dependency is removed and cascaded. What I would like is a way to control that based on a parent to child relationship from the delete() method of the ActiveRecord instance so it may be decided at run time. I would also like to be able to contorl it from the model files themselves where the runtime options would override those in the model files.
Bookmarks