<?php
/**
* Copyright (c) 2008, SARL Adaltas. All rights reserved.
* Code licensed under the BSD License:
* http://porte.adaltas.com/en/developer/license.html
*/
/**
* PorteManyToManyListTransient
* This class implement a many-to-many strategy with a column storing list of foreign keys
*
* @package Porte
* @subpackage plugin
* @author David Worms info(at)adaltas.com
* @copyright 2008 Adaltas
*/
class PorteManyToManyListPersisted{
/**
* Note, except the last two lines (the class name which could be provided as argument)
* code is identical to PorteManyToManyListTransient::setRecords
*
* @return PorteRecord Current record provided as 2nd argument
* @param $property String Name of the property being accessed
* @param $record PorteRecord Current record
* @param $assocRecord mixed (string,int,array,PorteRecord) Record to be associated with the current record
*/
public static function setRecords($property,$record,$assocRecords,$options=array()){
if(!empty($options['convert'])&&array_key_exists($property,$record->attributes)&&$record->attributes[$property]==$assocRecords&&!isset($record->associations[$property])){
return $record;
}
$config = $record->porte->models->{$record->type}['properties'][$property];
$assocRecords = new PorteIterator($record->porte->tables->{$config['has_many']['type']},$assocRecords);
// Extract all the primary keys to be set
$primaryKeysToSet = $assocRecords->getIdentifier();
// we delete all the associations which are not provided as arguments
$existingAssocRecords = $record->{'get'.PorteUtils::camelize($property)}();
$toDeleteAssocRecords = array();
$toSetAssocRecords = array();
foreach($existingAssocRecords as $existingAssocRecord){
if(!$existingAssocRecord->isNew()&&!in_array($existingAssocRecord->getIdentifier(),$primaryKeysToSet)){
$toDeleteAssocRecords[] = $existingAssocRecord;
}else if($existingAssocRecord->isNew()){
$isPresent = false;
foreach($assocRecords as $assocRecord){
if($assocRecord===$existingAssocRecord){
$isPresent = true;
}
}
if(!$isPresent){
$toDeleteAssocRecords[] = $existingAssocRecord;
}
}
}
$existingAssocRecords->rewind();
if(count($toDeleteAssocRecords))
PorteManyToManyListPersisted::deleteRecords($property,$record,$toDeleteAssocRecords);
return PorteManyToManyListPersisted::addRecords($property,$record,$assocRecords);
}
/**
* many-to-many with a column storing list of foreign keys
*
* @return PorteIterator Iterator containing the associated records
* @param $property String Name of the property being accessed
* @param $record PorteRecord
* @param $assocRecords PorteIterator Records to be associated with the current record
*/
public static function addRecord($property,$record,$assocRecord){
return self::addRecords($property,$record,$assocRecord);
}
/**
* many-to-many with a join table
*
* @return PorteIterator Iterator containing the associated records
* @param $property String Name of the property being accessed
* @param $record PorteRecord
* @param $assocRecords PorteIterator Records to be associated with the current record
*/
public static function addRecords($property,$record,$assocRecords){
$config = $record->porte->models->{$record->type}['properties'][$property];
$assocTable = $record->porte->tables->{$config['has_many']['type']};
$assocRecords = new PorteIterator($assocTable,$assocRecords);
if(!count($assocRecords)) return $record;
if(!array_key_exists($property,$record->associations)&&!$record->isNew()) $record->load($record->getIdentifier(),array('force'=>true));
if(!isset($record->associations[$property])){
$record->associations[$property] = new PorteIterator(
$assocTable,
// Preload PorteIterator with primary_keys
(isset($record->attributes[$property]))?explode(',',$record->attributes[$property]):array()
);
}
foreach($assocRecords as $assocRecord){
$skip = false;
// Make sure we do not create twice the same association
foreach($record->associations[$property] as $registeredRecord){
if($assocRecord->isNew() && $registeredRecord===$assocRecord){
$skip = true;
break;
}else if(!$assocRecord->isNew()&&$registeredRecord->getIdentifier() == $assocRecord->getIdentifier()){
$skip = true;
break;
}
}
$record->associations[$property]->rewind();
if(!$skip){
/** todo: following code in an attempt to update association reference
if($assocRecord instanceof PorteRecord){
$config = $record->model->properties->$property;
$assocModel = $record->porte->models->get($config['has_many']['type']);
$assocProperty = $config['has_many']['property'];
if(!isset($assocRecord->associations[$assocProperty]))
$assocRecord->associations[$assocProperty] =
new PorteIterator($assocModel,
isset($assocRecord->attributes[$assocProperty])?$assocRecord->attributes[$assocProperty]:array());
$assocRecord->associations[$assocProperty]->array[] = $record;
}
*/
$record->associations[$property]->array[] = $assocRecord;
}
}
return $record;
}
/**
* many-to-many with a join table
*
* @return PorteIterator Iterator containing the associated records
* @param $property String Name of the property being accessed
* @param $record PorteRecord
* @param $options array[optional] Optional array
*/
public static function getRecords($property,$record,$options=array()){
$model = $record->porte->models->{$record->type};
$propertyModel = $model['properties'][$property];
$assocModel = $record->porte->models->{$propertyModel['has_many']['type']};
$assocTable = $record->porte->tables->{$assocModel['type']};
if(isset($record->associations[$property])){
return $record->associations[$property];
}else if($record->isNew()){
return $record->associations[$property] = new PorteIterator($assocTable,array());
}else{
$fks = isset($record->attributes[$property])?explode(',',$record->attributes[$property]):array();
$primaryKey = $model['primary_key'];
$primaryKeyValue = $record->getIdentifier();
$options = array_merge(array(
'select'=>' `'.$assocModel['database'].'`.`'.$assocModel['table'].'`.* ',
'from'=>'`'.$assocModel['database'].'`.`'.$assocModel['table'].'`,`'.$model['database'].'`.`'.$model['table'].'`',
'where'=>'1 ',
),$options);
$options['where'] .= ' AND `'.$model['database'].'`.`'.$model['table'].'`.`'.$primaryKey.'` = \''.$primaryKeyValue.'\' AND FIND_IN_SET(`'.$assocModel['database'].'`.`'.$assocModel['table'].'`.`'.$assocModel['primary_key'].'`, `'.$model['database'].'`.`'.$model['table'].'`.`'.$propertyModel['field'].'`)';
return $record->associations[$property] = $assocTable->find($options);
}
}
public static function deleteRecord($property,$record,$assocRecord=array()){
self::deleteRecords($property,$record,$assocRecord);
}
/**
* Note, at the moment, we do not re-index the internal iterator array after unsetting a record
*
* @return null
* @param $property string
* @param $record PorteRecord
* @param $deletedRecords Object[optional]
*/
public static function deleteRecords($property,PorteRecord $record,$deletedRecords=array()){
$record->events->connect('record_save_before',array('PorteManyToManyListPersisted','deleteRecords_saveBefore'));
$config = $record->porte->models->{$record->type}['properties'][$property];
$assocTable = $record->porte->tables->{$config['has_many']['type']};
if(empty($deletedRecords)){
if(!isset($record->associations[$property])){
$record->associations[$property] = new PorteIterator($assocTable);
}else{
$record->associations[$property]->arrayToDelete = $record->associations[$property]->array;
$record->associations[$property]->array = array();
}
return $record;
}
if(is_string($deletedRecords)){
$deletedRecords = explode(',',$deletedRecords);
}else if(is_int($deletedRecords)){
$deletedRecords = array(strval($deletedRecords));
}else if(is_array($deletedRecords)){
foreach($deletedRecords as $k=>$assocRecord){
if(is_object($assocRecord)){
$deletedRecords[$k] = strval($assocRecord->getIdentifier());
}else if(is_int($assocRecord)||ctype_digit($assocRecord)){
$deletedRecords[$k] = strval($assocRecord);
}
}
}else if($deletedRecords instanceof PorteRecord){
$deletedRecords = array($deletedRecords->getIdentifier());
}
if(isset($record->associations[$property])){
$assocRecords = &$record->associations[$property];
while($assocRecord = $assocRecords->current()){
if(in_array($assocRecord->getIdentifier(),$deletedRecords)){
if(!isset($assocRecords->arrayToDelete)) $assocRecords->arrayToDelete = array();
$assocRecords->arrayToDelete[] = $assocRecord;
$assocRecords->shift();
}else{
$assocRecords->next();
}
}
$assocRecords->rewind();
}
return $record;
}
public static function deleteRecords_saveBefore(PorteRecord $record,&$circular){
foreach($record->associations as $property=>$assocRecords){
$idsToDelete = array();
if(isset($assocRecords->arrayToDelete)){
foreach($assocRecords->arrayToDelete as $v){
if(is_string($v)){
$idsToDelete[] = explode(',',$v);
}else if(is_int($v)){
$idsToDelete[] = strval($v);
}else if($v instanceof PorteRecord){
$idsToDelete[] = $v->getIdentifier();
}
}
$record->attributes[$property] = implode(',',array_diff(explode(',',$record->attributes[$property]),$idsToDelete));
}
}
}
public static function countRecords($property,$record,$options=array()){
$model = $record->porte->models->{$record->type};
$propertyModel = $model['properties'][$property];
$assocModel = $record->porte->models->{$model['properties'][$property]['has_many']['type']};
//$assocTable = $record->porte->tables->{$assocModel['type']};
$fks = explode(',',$record->attributes[$property]);
$options = array_merge(array(
'select'=>' count(*) ',
'from'=>'`'.$assocModel['database'].'`.`'.$assocModel['table'].'`,`'.$model['database'].'`.`'.$model['table'].'`',
'where'=>'1',
),$options);
$options['where'] .= ' AND `'.$model['database'].'`.`'.$model['table'].'`.`'.$model['primary_key'].'` = \''.$record->getIdentifier().'\' AND FIND_IN_SET(`'.$assocModel['database'].'`.`'.$assocModel['table'].'`.`'.$assocModel['primary_key'].'`, `'.$model['database'].'`.`'.$model['table'].'`.`'.$propertyModel['field'].'`)';
// todo: why are we not calling find without execute?
return intval($record->porte->query($record->table->find(array_merge($options,array('return_sql'=>true,'limit'=>null))))->fetchColumn());
}
}
?>