<?php

require_once 'orm/OrmWorker.php';

/**
 * Worker class for propagate saving objects.
 *
 * This class handles object saving for relations. Save could mean insert or 
 * update. And, in some cases, it could even mean delete (in a one to many 
 * relation, for example, an update may carry the need to delete some of the 
 * other table's records.
 *
 * @author  $Author: pruna $
 * @package orm/worker
 * @version $Revision: 1.3 $
 * @created 19-Oct-2005
 * @modified $Date: 2005/12/06 02:56:45 $
 */
class OrmWorkerSave extends OrmWorker {
    
    /**
     * Save action for relations.
     *
     * It includes o2m and m2m, invoked after saving main.
     * Allowed operations are 'insert', 'update', 'insert-only', 'update-only'
     *
     * @access  public
     * @param   OrmMap        $map           Map to use
     * @param   Bean          $bean          Bean to save
     * @param   string        $operation     Operation to perform
     */
    public function saveRelations($map, $bean, $operation) {
        $id = $this->getter($bean, $map->getId());
        
        // o2m
        $this->o2m($map, $bean, $operation, $id);
        
        // m2m
        $this->m2m($map, $bean, $operation, $id);
    }

    /**
     * Apply save action to o2m relations.
     *
     * Allowed operations are 'insert', 'update', 'insert-only', 'update-only'
     *
     * @access  protected
     * @param   OrmMap        $map           Map to use
     * @param   Bean          $bean          Bean to save
     * @param   string        $operation     Operation to perform
     * @param   mixed         $id            Id to use
     */
    protected function o2m($map, $bean, $operation, $id) {
        foreach ($map->getO2ms() as $attr) {
            if ($this->isCascadeAccepted($attr, $operation) && 
                    (!$attr->isLazy() || !$bean->stillLazy($attr->getName()))) {
                
                $dao = new OrmCore($attr->getFields(), $this->getDao());
                
                // get right bean's property pointing to left bean, 
                // to assign key from left bean (default: PK obtained), in $id
                $setterPropRef = null;
                foreach ($attr->getFields()->getProps() as $innerAttr) {
                    if ($innerAttr->getColumn() == $attr->getPropertyRef() 
                            || $innerAttr->getName() == $attr->getPropertyRef()) {
                        
                        $setterPropRef = 
                                $this->getBu()->setterName($innerAttr->getName());
                    }
                }

                // if doesn't found an explicit property for back-reference, 
                // use with property-ref field.
                if (is_null($setterPropRef)) {
                    $setterPropRef =
                            $this->getBu()->setterName($attr->getPropertyRef());
                }
                
                // if still not found, throw exception
                if (is_null($setterPropRef)) {
                    throw new OrmException('Can\'t locate back-reference ' .
                            'in o2m relation', 
                            OrmException::CODE_BACK_REFERENCE_ERROR, $this);
                }
                
                // Value from left bean (default: PK obtained)
                //$key = $this->getter($bean, $attr);
                
                // @todo see if "null" has sense, or should be some kind of empty
                if (($beans = $this->getter($bean, $attr)) !== null) {
                    // values found, save them
                    foreach ($beans as $innerBean) {
                        // change other bean's prop pointing to this, with new PK
                        $innerBean->$setterPropRef($id);
                        
                        $ret = $this->propagateAction($attr, $operation, 
                                $dao, $innerBean);
                    }
                } else {
                    if ($this->isCascadeAccepted($attr, 'clean')) {
                        // delete rows pointing to this, with ad-hoc query
                        $sql = $this->getDb()->createSqlDelete();
                        $sql->addToTable($attr->getTable());
                        $sql->addToWhere($attr->getTable() . '.' 
                                . $attr->getPropertyRef() . ' = ?');
                        $sql->addToValues($id);
                        
                        $this->getDao()->getDb()->delete($sql);
                    } elseif ($this->isCascadeAccepted($attr, 'purge')) {
                        // get full beans pointing to this, and then delete them
                        /* @todo: built this. Pseudo-code:
                        $beans = $this->getDao()->search();
                        foreach ($beans) { delete }
                        */
                    }
                }
            }
        }
    }
            
    /**
     * Apply save action to m2m relations.
     *
     * Allowed operations are 'insert', 'update', 'insert-only', 'update-only'
     *
     * @access  protected
     * @param   OrmMap        $map           Map to use
     * @param   Bean          $bean          Bean to save
     * @param   string        $operation     Operation to perform
     * @param   mixed         $id            Id to use
     */
    protected function m2m($map, $bean, $operation, $id) {
        foreach ($map->getM2ms() as $attr) {
            // the other 'main' table
            if ($this->isCascadeAccepted($attr, $operation) 
                    && (!$attr->isLazy() 
                    || !$bean->stillLazy($attr->getName()))) {
                
                $dao = new OrmCore($attr->getFields(), $this->getDao());
                
                foreach ($this->getter($bean, $attr) as $innerBean) {
                    $ret = $this->propagateAction($attr, $operation, $dao, 
                            $innerBean);
                }
            }
            // the pivot table
            if ($attr->inCascadePivot($operation)) {
                // in pivot doesn't allow the 'only' operations
                
                // Value from left bean (default: PK obtained)
                //$key = $this->getter($bean, $attr);
                
                // @todo now: remove everything, and later insert existent
                // option: split operations (upd, ins, del)
                $this->removeM2mPivot($map, $bean, $attr, $id);
                
                // Create base insert
                $sql = $this->getDb()->createSqlInsert();
                // add relation to left (allways the same)
                $sql->addToTable($attr->getPivotTable());
                $sql->addToColumns($attr->getPivotTable() . '.' 
                        . $attr->getPivotToLeft());
                $sql->addToValues($id);
                
                // get other bean's PK (where pivot points), to assign 
                // later to pivot table
                $getterPropRef = 
                        $this->getBu()->getterName(
                        $attr->getFields()->getId()->getName());
                
                foreach ($this->getter($bean, $attr) as $innerBean) {
                    $sql_clon = clone $sql;
                    // if needs a PK, add it
                    $id = $this->getDb()->getCalculatedPK($bean, $this->getDao());
                    if ($id != null) {
                        if ($attr->getPivotColumnPk() == null) {
                            throw new OrmException("Can\'t locate pivot's PK column ", 
                            OrmException::CODE_MISSING_PIVOT_COLUMN_PK, $this);
                        }
                        $this->addIdToInsert($sql_clon, $attr->getPivotTable(), 
                                $attr->getPivotColumnPk(), $id);
                    }
                    // add relation to right
                    $sql_clon->addToColumns($attr->getPivotTable() . '.' 
                            . $attr->getPivotToRight());
                    $sql_clon->addToValues($innerBean->$getterPropRef());
                    
                    $this->getDao()->getDb()->insert($sql_clon);
                }
            }
        }
    }

    /**
     * Remove pivot table used in m2m relations.
     * 
     * <strong>Note:</strong> this method actually belongs to 
     * <em>OrmWorkerRemove</em>, and is duplicated here because its used in 
     * the current implementation of <em>m2m</em> method. The idea is to 
     * modify <em>m2m</em> method soon, and remove this method from here. 
     * Currently, I think is easier to duplicate it here, that try to get a 
     * <em>OrmWorkerRemove</em>'s instance here, or refactor (move) this method 
     * to the common parent. All this is the reason to mark it as "deprecated".
     * 
     * @deprecated 
     * @access  protected
     * @param   OrmMap         $map        Map used 
     * @param   Bean           $bean       Bean to get Id from
     * @param   OrmMapElement  $attr       Attribute
     * @param   mixed          $id         Id to use
     * @return  mixed
     */
    protected function removeM2mPivot($map, $bean, $attr, $id = null) {
        // can't use DAO like others, because this is a 'special' structure
        // for pivot tables. So, this method creates an ad-hoc OrmSqlDelete
        if (is_null($id)) {
            $id = $this->getter($bean, $map->getId());
        }
        
        $sql = $this->getDb()->createSqlDelete();
        $sql->addToTable($attr->getPivotTable());
        $sql->addToWhere($attr->getPivotTable() . '.' 
                . $attr->getPivotToLeft() . ' = ?');
        $sql->addToValues($id);
        
        return $this->getDao()->getDb()->delete($sql);
    }
    
    /**
     * Prepare save action for relations.
     *
     * It includes o2o and m2o, invoked before saving main.
     * Postfix parameter is used to add "=?", to place query's values for update
     *
     * @access  public
     * @param   OrmMap        $map             Map to use
     * @param   Bean          $bean            Bean to save
     * @param   string        $operation       Operation to perform
     * @param   OrmSql        $sql             SQL to prepare
     * @param   string        $columnsPostFix  Postfix for columns
     */
    public function prepareSave($map, $bean, $operation, $sql, 
            $columnsPostFix = '', $addIdToColumns = FALSE) {
        
        $sql->addToTable($map->getTable());
        
        // doesn't matters if used 'select' or 'join', all updates, inserts and
        // deletes must be done in separated querys
        
        // m2o and o2o must be done before this, because I could need one of 
        // theirs generated PK
        
        // m2o
        $this->m2o($map, $bean, $operation, $sql, $columnsPostFix);
        
        // o2o
        $this->o2o($map, $bean, $operation, $sql, $columnsPostFix);
        
        // @todo problem with props: if I add all (instead of only those that
        // have changed), if one prop is mapped twice and only one changes, it
        // depends on if we changed the first or the second to know it will
        // actually change or not.
        
        // id
        if ($addIdToColumns) {
            $attr = $map->getId();
            $getter = $this->getBu()->getterName($attr->getName());
            $this->addIdToInsert($sql, $map->getTable(), $attr->getColumn(), $this->getter($bean, $attr));
        }
        
        // props
        foreach ($map->getProps() as $attr) {
            if ($this->isOperationAccepted($attr, $operation) 
                    && $attr->getFormula() == '') {
                
                $sql->addToColumns($map->getTable() . '.' . $attr->getColumn() 
                        . $columnsPostFix);
                $getter = $this->getBu()->getterName($attr->getName());
                $sql->addToValues($this->getter($bean, $attr));
            }
        }
        
        return $sql;
    }
    
    /**
     * Add pk column to insert querys.
     *
     * There are some cases when you need to add the pk column to inserts.
     * Currently, it's used when PK is pre-calculated (using getCalculatedPK) 
     * and also when working with special cases, like m2m's pivot table.
     *
     * @access  protected
     * @param   OrmSqlInsert  $sql              SQL where add data
     * @param   string        $table            Table name
     * @param   string        $column           Column name
     * @param   mixed         $value            Value to insert
     */
    protected function addIdToInsert($sql, $table, $column, $value) {
        $sql->addToColumns($table . '.' . $column);
        $sql->addToValues($value);
    }
    
    /**
     * Prepare save action for m2o relations.
     *
     * @access  protected
     * @param   OrmMap        $map             Map to use
     * @param   Bean          $bean            Bean to save
     * @param   string        $operation       Operation to perform
     * @param   OrmSql        $sql             SQL to prepare
     * @param   string        $columnsPostFix  Postfix for columns
     */
    protected function m2o($map, $bean, $operation, $sql, $columnsPostFix) {
        foreach ($map->getM2os() as $attr) {
            if (!$attr->isLazy() || !$bean->stillLazy($attr->getName())) {
                $getter = $this->getBu()->getterName($attr->getName());
                
                if ($this->isCascadeAccepted($attr, $operation)) {
                    if ($attr->isNullable() && $this->isNullValue($other_bean)) {
                        // no need to go further, because related object 
                        // doesn't exist or should be deleted.
                        // @todo do remove (or prune, or similar) if configured
                    } else {
                        $dao = new OrmCore($attr->getFields(), $this->getDao());
                        
                        $ret = $this->propagateAction($attr, $operation, $dao, 
                                $bean->$getter());
                    }
                }
                
                if ($this->isOperationAccepted($attr, $operation)) {
                    $sql->addToColumns($map->getTable() . '.' 
                            . $attr->getColumn() . $columnsPostFix);
                    $m2o_pk_getter = $this->getBu()->getterName(
                            $attr->getFields()->getId()->getName());
                    $sql->addToValues($bean->$getter()->$m2o_pk_getter());
                }
            }
        }
    }
    
    /**
     * Prepare save action for o2o relations.
     *
     * @access  protected
     * @param   OrmMap        $map             Map to use
     * @param   Bean          $bean            Bean to save
     * @param   string        $operation       Operation to perform
     * @param   OrmSql        $sql             SQL to prepare
     * @param   string        $columnsPostFix  Postfix for columns
     */
    protected function o2o($map, $bean, $operation, $sql, $columnsPostFix) {
        foreach ($map->getO2os() as $attr) {
            if (!$attr->isLazy() || !$bean->stillLazy($attr->getName())) {
                
                // @todo missing check if $attr->isConstrained() == true
                // if false, it should be done later, in $this->saveRelations(), 
                // but I'm not sure if something else needs to be donde there
                if ($attr->isExtends()) {
                    $other_bean = $bean;
                } else {
                    $other_bean = $this->getter($bean, $attr);
                }
                if ($this->isCascadeAccepted($attr, $operation)) {
                    if ($attr->isNullable() && $this->isNullValue($other_bean)) {
                        // no need to go further, because related object 
                        // doesn't exist or should be deleted.
                        // @todo do remove (or prune, or similar) if configured
                    } else {
                        $dao = new OrmCore($attr->getFields(), $this->getDao());
                        /*if ($attr->isExtends()) {
                            
                        }*/
                        $ret = $this->propagateAction($attr, $operation, $dao, 
                                $other_bean);
                    }
                }
                
                if ($this->isOperationAccepted($attr, $operation)) {
                    $sql->addToColumns($map->getTable() . '.' 
                            . $attr->getColumn() . $columnsPostFix);
                    
                    // o2o_pk_getter
                    $sql->addToValues($this->getter($other_bean, 
                            $attr->getFields()->getId()));
                }
            }
        }
    }
    
    /**
     * Checks if cascade is accepted for this $operation.
     *
     * @access  protected
     * @param   OrmMapElement  $attr          Attribute
     * @param   string         $operation     Operation to be propagated
     * @return  boolean
     */
    protected function isCascadeAccepted($attr, $operation) {
        return ($attr->inCascade($operation) 
                || $attr->inCascade($operation.'-only'));
    }
    
    /**
     * Checks if field can be modified for this $operation.
     *
     * @access  protected
     * @param   OrmMapElement  $attr          Attribute
     * @param   string         $operation     Operation to be propagated
     * @return  boolean
     */
    protected function isOperationAccepted($attr, $operation) {
        $operation = str_replace('-only', '', $operation);
        return $this->getBu()->getter($attr, $operation);
    }
    
    /**
     * Propagates save actions.
     * 
     * It will cascade a "save" if $operation is in cascade.
     * Else, it will cascade only this $operation.
     *
     * @access  protected
     * @param   OrmMapElement  $attr          Attribute
     * @param   string         $operation     Operation to be propagated
     * @param   OrmIDAO        $dao           DAO object
     * @param   Bean           $bean          Bean to save
     * @return  Result of propagated action.
     */
    protected function propagateAction($attr, $operation, $dao, $bean) {
        if ($attr->inCascade($operation)) {
            return $dao->save($bean);
        } else {
            // '*-only' was defined, call only this $operation
            return $dao->$operation($bean);
        }
    }
    
}

?>