Hi! I faced some problems with current Model implementation:
1) There is a bug with implicit transactions on saving model with not valid related models when using "exceptionOnFailedSave" enabled and nested transactions via savePoints:
$song = new Songs();
$album = new Albums();
$album->someRequiredProp = null;
$song->Album = $album;
$song->save();
// call $song->_preSaveRelatedRecords();
// begin implicit transaction
// call $album->save(); // throws an Exception because of empty someRequiredProp.
// no expected rollback() for implicit transaction here because exception is not handled in $song.
// this causes savepoint not exists exception next time rollback() is called.
2) I really don't like magic props and getters confusing behavior especially on saving related models:
$song = new Songs();
$album = new $album;
$album->title = 'foo';
$song->Album = $album;
$song->save();
$album = $song->getAlbum();
$album->title = 'bar';
$album->save();
echo $song->getAlbum()->title; // 'bar'
$song->save();
echo $song->getAlbum()->title; // 'foo'
// wtf?! why saving old related $album object with old title again when it's not actual anymore? we expect only $song object to be saved, because $album is not dirty anymore. We should keep only dirty related objects.
3) I prefer to use getters/setters approach only and would like to see magic setter for related objects.
Here is base class solving all those problems for me:
abstract class Model extends \Phalcon\MVC\Model
{
public function save($data = null, $whiteList = null)
{
/**
* Override save() method to handle
* possible relations recursion and
* cleanup related models bag after save.
*/
$related = $this->_related;
try {
$result = parent::save($data, $whiteList);
if (!$result) {
$this->_related = $related;
}
return $result;
} catch (\Exception $e) {
$this->_related = $related;
throw $e;
}
}
protected function _preSaveRelatedRecords($conn, $related)
{
/**
* Fix related models recursion problem.
* Fix savepoint exception when using nested transactions
* with model exceptionOnFailedSave.
*/
$this->_related = null;
try {
$txLevel = $conn->getTransactionLevel();
return parent::_preSaveRelatedRecords($conn, $related);
} catch (\Exception $e) {
while ($conn->getTransactionLevel() > $txLevel) {
try {
$conn->rollback();
} catch (\PDOException $pdoEx) {}
}
throw $e;
}
}
public function __call($method, $args = null)
{
/**
* Add magic setter for related models.
*/
if (1 === count($args)
&& ($args[0] instanceof \Phalcon\MVC\ModelInterface || is_array($args[0]))
&& (0 === stripos($method, 'set'))
) {
$related = substr($method, 3);
$this->$related = $args[0];
return $this;
}
return parent::__call($method, $args);
}
}
Or am I just misunderstanding some basics and this is how it's designed? :-)