Copyable Behavior
Copyable Behavior adds the ability to copy a model record, including all hasMany and hasAndBelongsToMany
associations. Copyable relies on Containable behavior, which this behavior will attach on the fly as needed. HABTM relationships are just duplicated in the join table, while hasMany and hasOne records are recursively copied as well.
associations. Copyable relies on Containable behavior, which this behavior will attach on the fly as needed. HABTM relationships are just duplicated in the join table, while hasMany and hasOne records are recursively copied as well.
Copyable adds a copy() function to your model, which you can use to copy (that is, create a duplicate of) a record and any of its hasOne, hasMany, or hasAndBelongsToMany relationships. In the case of hasOne and hasMany, those records are recursively copied as well. For example, if you want to copy a LinkCategory that hasMany Link, all of the Link records will be copied. The copy is fully recursive, meaning that if Link HasMany Comment, then all of those records will be copied as well. In the case of HABTM associations, only the join table rows are copied, not the associated records. The copy() function takes one argument – the ID of the record you wish to copy.
The code is on page two, but the most current version is kept on Github: http://github.com/jamienay/copyable_behavior
Copyable uses Containable to help generate its queries, but don’t worry – it’ll attach Containable if it can’t find it on the model.
A handful of config options:
* recursive: whether to copy hasOne- and hasMany-associated models (default: true)
* habtm: whether to copy hasAndBelongsToMany relationships (default: true)
* stripFields: an array model fields that should ignored when copying (default: id, created, modified, lft, rght)
After attaching Copyable to a model via the $actsAs array – I recommend putting it on AppModel – usage is as simple as:
The code is on page two, but the most current version is kept on Github: http://github.com/jamienay/copyable_behavior
Copyable uses Containable to help generate its queries, but don’t worry – it’ll attach Containable if it can’t find it on the model.
A handful of config options:
* recursive: whether to copy hasOne- and hasMany-associated models (default: true)
* habtm: whether to copy hasAndBelongsToMany relationships (default: true)
* stripFields: an array model fields that should ignored when copying (default: id, created, modified, lft, rght)
After attaching Copyable to a model via the $actsAs array – I recommend putting it on AppModel – usage is as simple as:
// From a controller method
$this->MyModel->copy($id);
// From a model method
$this->copy($id);
Behavior Class:
<?php
/**
* Copyable Behavior class file.
*
* Adds ability to copy a model record, including all hasMany and hasAndBelongsToMany
* associations. Relies on Containable behavior, which this behavior will attach
* on the fly as needed.
*
* HABTM relationships are just duplicated in the join table, while hasMany and hasOne
* records are recursively copied as well.
*
* Usage is straightforward:
* From model: $this->copy($id); // id = the id of the record to be copied
* From container: $this->MyModel->copy($id);
*
* @filesource
* @author Jamie Nay
* @copyright Jamie Nay
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
* @link http://github.com/jamienay/copyable_behavior
*/
class CopyableBehavior extends ModelBehavior {
/**
* Behavior settings
*
* @access public
* @var array
*/
public $settings = array();
/**
* Array of contained models.
*
* @access public
* @var array
*/
public $contain = array();
/**
* The full results of Model::find() that are modified and saved
* as a new copy.
*
* @access public
* @var array
*/
public $record = array();
/**
* Default values for settings.
*
* - recursive: whether to copy hasMany and hasOne records
* - habtm: whether to copy hasAndBelongsToMany associations
* - stripFields: fields to strip during copy process
*
* @access private
* @var array
*/
private $defaults = array(
'recursive' => true,
'habtm' => true,
'stripFields' => array('id', 'created', 'modified', 'lft', 'rght')
);
/**
* Configuration method.
*
* @param object $Model Model object
* @param array $config Config array
* @access public
* @return boolean
*/
public function setup($Model, $config = array()) {
$this->settings[$Model->alias] = array_merge($this->defaults, $config);
return true;
}
/**
* Copy method.
*
* @param object $Model model object
* @param mixed $id String or integer model ID
* @access public
* @return boolean
*/
public function copy($Model, $id) {
$this->generateContain($Model);
$this->record = $Model->find('first', array(
'conditions' => array($Model->alias.'.id' => $id),
'contain' => $this->contain
));
if (empty($this->record)) {
return false;
}
if (!$this->__convertData($Model)) {
return false;
}
return $this->__copyRecord($Model);
}
/**
* Wrapper method that combines the results of __recursiveChildContain()
* with the models' HABTM associations.
*
* @param object $Model Model object
* @access public
* @return array;
*/
public function generateContain($Model) {
if (!$this->__verifyContainable($Model)) {
return false;
}
$this->contain = array_merge($this->__recursiveChildContain($Model), array_keys($Model->hasAndBelongsToMany));
return $this->contain;
}
/**
* Strips primary keys and other unwanted fields
* from hasOne and hasMany records.
*
* @param object $Model model object
* @param array $record
* @access private
* @return array $record
*/
private function __convertChildren($Model, $record) {
$children = array_merge($Model->hasMany, $Model->hasOne);
foreach ($children as $key => $val) {
if (!isset($record[$key]) || empty($record[$key])) {
continue;
}
if (isset($record[$key][0])) {
foreach ($record[$key] as $innerKey => $innerVal) {
$record[$key][$innerKey] = $this->__stripFields($Model, $innerVal);
if (array_key_exists($val['foreignKey'], $innerVal)) {
unset($record[$key][$innerKey][$val['foreignKey']]);
}
$record[$key][$innerKey] = $this->__convertChildren($Model->{$key}, $record[$key][$innerKey]);
}
} else {
$record[$key] = $this->__stripFields($Model, $record[$key]);
if (isset($record[$key][$val['foreignKey']])) {
unset($record[$key][$val['foreignKey']]);
}
$record[$key] = $this->__convertChildren($Model->{$key}, $record[$key]);
}
}
return $record;
}
/**
* Strips primary and parent foreign keys (where applicable)
* from $this->record in preparation for saving.
*
* @param object $Model Model object
* @access private
* @return array $this->record
*/
private function __convertData($Model) {
$this->record[$Model->alias] = $this->__stripFields($Model, $this->record[$Model->alias]);
$this->record = $this->__convertHabtm($Model, $this->record);
$this->record = $this->__convertChildren($Model, $this->record);
return $this->record;
}
/**
* Loops through any HABTM results in $this->record and plucks out
* the join table info, stripping out the join table primary
* key and the primary key of $Model. This is done instead of
* a simple collection of IDs of the associated records, since
* HABTM join tables may contain extra information (sorting
* order, etc).
*
* @param object $Model Model object
* @access public
* @return array modified $record
*/
private function __convertHabtm($Model, $record) {
if (!$this->settings[$Model->alias]['habtm']) {
return $record;
}
foreach ($Model->hasAndBelongsToMany as $key => $val) {
if (!isset($record[$val['className']]) || empty($record[$val['className']])) {
continue;
}
$joinInfo = Set::extract($record[$val['className']], '{n}.'.$val['with']);
if (empty($joinInfo)) {
continue;
}
foreach ($joinInfo as $joinKey => $joinVal) {
$joinInfo[$joinKey] = $this->__stripFields($Model, $joinVal);
if (array_key_exists($val['foreignKey'], $joinVal)) {
unset($joinInfo[$joinKey][$val['foreignKey']]);
}
}
$record[$val['className']] = $joinInfo;
}
return $record;
}
/**
* Performs the actual creation and save.
*
* @param object $Model Model object
* @access private
* @return mixed
*/
private function __copyRecord($Model) {
$Model->create();
$Model->set($this->record);
return $Model->saveAll();
}
/**
* Generates a contain array for Containable behavior by
* recursively looping through $Model->hasMany and
* $Model->hasOne associations.
*
* @param object $Model Model object
* @access private
* @return array
*/
private function __recursiveChildContain($Model) {
$contain = array();
if (!$this->settings[$Model->alias]['recursive']) {
return $contain;
}
$children = array_merge(array_keys($Model->hasMany), array_keys($Model->hasOne));
foreach ($children as $child) {
$contain[$child] = $this->__recursiveChildContain($Model->{$child});
}
return $contain;
}
/**
* Strips unwanted fields from $record, taken from
* the 'stripFields' setting.
*
* @param object $Model Model object
* @param array $record
* @access private
* @return array
*/
private function __stripFields($Model, $record) {
foreach ($this->settings[$Model->alias]['stripFields'] as $field) {
if (array_key_exists($field, $record)) {
unset($record[$field]);
}
}
return $record;
}
/**
* Attaches Containable if it's not already attached.
*
* @param object $Model Model object
* @access private
* @return boolean
*/
private function __verifyContainable($Model) {
if (!$Model->Behaviors->attached('Containable')) {
return $Model->Behaviors->attach('Containable');
}
return true;
}
}
?>








First off, nice behavior. I've been using it in a current project with success. I had to add an additional param to the find statement (line:92-85). I added 'callbacks' => false to the find query. If you're doing any kind of data manipulation in an afterFind callback, the copy may not work correct. I was reformatting dates for display in an afterFind and they would not insert correct. After adding the callbacks => false, works great.
Comments are closed for articles over a year old