Skip to content

Commit

Permalink
Merge pull request #2217 from cakephp/3.0-entity-save
Browse files Browse the repository at this point in the history
Implementing the save method in 3.0
  • Loading branch information
lorenzo committed Oct 30, 2013
2 parents df30e5e + 9b66877 commit e540c1b
Show file tree
Hide file tree
Showing 18 changed files with 1,405 additions and 57 deletions.
49 changes: 39 additions & 10 deletions Cake/Database/Connection.php
Expand Up @@ -438,6 +438,45 @@ public function rollbackSavepoint($name) {
$this->execute($this->_driver->rollbackSavePointSQL($name));
}

/**
* Executes a callable function inside a transaction, if any exception occurs
* while executing the passed callable, the transaction will be rolled back
* If the result of the callable function is ``false``, the transaction will
* also be rolled back. Otherwise the transaction is committed after executing
* the callback
*
* The callback will receive the connection instance as its first argument..
*
* ### Example:
*
* {{{
* $connection->transactional(function($connection) {
* $connection->newQuery()->delete('users')->execute();
* });
* }}}
*
* @param callable $callback the code to be executed inside a transaction
* @return mixed result from the $callback function
*/
public function transactional(callable $callback) {
$this->begin();

try {
$result = $callback($this);
} catch (\Exception $e) {
$this->rollback();
throw $e;
}

if ($result === false) {
$this->rollback();
return false;
}

$this->commit();
return $result;
}

/**
* Quotes value to be used safely in database query
*
Expand Down Expand Up @@ -470,16 +509,6 @@ public function quoteIdentifier($identifier) {
return $this->_driver->quoteIdentifier($identifier);
}

/**
* Returns last id generated for a table or sequence in database
*
* @param string $table table name or sequence to get last insert value from
* @return string|integer
*/
public function lastInsertId($table) {
return $this->_driver->lastInsertId($table);
}

/**
* Enables or disables query logging for this connection.
*
Expand Down
14 changes: 14 additions & 0 deletions Cake/Database/Dialect/PostgresDialectTrait.php
Expand Up @@ -55,6 +55,20 @@ protected function _selectQueryTranslator($query) {
return $query;
}

/**
* Modifies the original insert query to append a "RETURNING *" epilogue
* so that the latest insert id can be retrieved
*
* @param Cake\Database\Query $query
* @return Cake\Database\Query
*/
protected function _insertQueryTranslator($query) {
if (!$query->clause('epilog')) {
$query->epilog('RETURNING *');
}
return $query;
}

/**
* Returns an dictionary of expressions to be transformed when compiling a Query
* to SQL. Array keys are method names to be called in this class
Expand Down
5 changes: 3 additions & 2 deletions Cake/Database/Driver.php
Expand Up @@ -164,10 +164,11 @@ public function schemaValue($value) {
* Returns last id generated for a table or sequence in database
*
* @param string $table table name or sequence to get last insert value from
* @param string column the name of the column representing the primary key
* @return string|integer
*/
public function lastInsertId($table = null) {
return $this->_connection->lastInsertId($table);
public function lastInsertId($table = null, $column = null) {
return $this->_connection->lastInsertId($table, $column);
}

/**
Expand Down
4 changes: 2 additions & 2 deletions Cake/Database/Driver/PDODriverTrait.php
Expand Up @@ -126,9 +126,9 @@ public function quote($value, $type) {
* @param string $table table name or sequence to get last insert value from
* @return string|integer
*/
public function lastInsertId($table = null) {
public function lastInsertId($table = null, $column = null) {
$this->connect();
return $this->_connection->lastInsertId();
return $this->_connection->lastInsertId($table);
}

/**
Expand Down
36 changes: 31 additions & 5 deletions Cake/Database/Query.php
Expand Up @@ -72,7 +72,8 @@ class Query implements ExpressionInterface, IteratorAggregate {
'order' => null,
'limit' => null,
'offset' => null,
'union' => []
'union' => [],
'epilog' => null
];

/**
Expand All @@ -91,6 +92,7 @@ class Query implements ExpressionInterface, IteratorAggregate {
'order' => ' %s',
'limit' => ' LIMIT %s',
'offset' => ' OFFSET %s',
'epilog' => ' %s'
];

/**
Expand Down Expand Up @@ -280,7 +282,10 @@ public function traverse(callable $visitor) {
* @return void
*/
protected function _traverseSelect(callable $visitor) {
$parts = ['select', 'from', 'join', 'where', 'group', 'having', 'order', 'limit', 'offset', 'union'];
$parts = [
'select', 'from', 'join', 'where', 'group', 'having', 'order', 'limit',
'offset', 'union', 'epilog'
];
foreach ($parts as $name) {
$visitor($this->_parts[$name], $name);
}
Expand All @@ -293,7 +298,7 @@ protected function _traverseSelect(callable $visitor) {
* @return void
*/
protected function _traverseDelete(callable $visitor) {
$parts = ['delete', 'from', 'where'];
$parts = ['delete', 'from', 'where', 'epilog'];
foreach ($parts as $name) {
$visitor($this->_parts[$name], $name);
}
Expand All @@ -306,7 +311,7 @@ protected function _traverseDelete(callable $visitor) {
* @return void
*/
protected function _traverseUpdate(callable $visitor) {
$parts = ['update', 'set', 'where'];
$parts = ['update', 'set', 'where', 'epilog'];
foreach ($parts as $name) {
$visitor($this->_parts[$name], $name);
}
Expand All @@ -319,7 +324,7 @@ protected function _traverseUpdate(callable $visitor) {
* @return void
*/
protected function _traverseInsert(callable $visitor) {
$parts = ['insert', 'values'];
$parts = ['insert', 'values', 'epilog'];
foreach ($parts as $name) {
$visitor($this->_parts[$name], $name);
}
Expand Down Expand Up @@ -1319,6 +1324,27 @@ public function delete($table = null) {
return $this;
}

/**
* A string or expression that will be appended to the generated query
*
* ### Examples:
* {{{
* $query->select('id')->where(['author_id' => 1])->epilog('FOR UPDATE');
* $query
* ->insert('articles', ['title'])
* ->values(['author_id' => 1])
* ->epilog('RETURNING id');
* }}}
*
* @params string|QueryExpression the expression to be appended
* @return Query
*/
public function epilog($expression = null) {
$this->_dirty();
$this->_parts['epilog'] = $expression;
return $this;
}

/**
* Returns the type of this query (select, insert, update, delete)
*
Expand Down
14 changes: 14 additions & 0 deletions Cake/Database/Statement/StatementDecorator.php
Expand Up @@ -264,4 +264,18 @@ public function bind($params, $types) {
}
}

/**
* Returns the latest primary inserted using this statement
*
* @param string $table table name or sequence to get last insert value from
* @param string column the name of the column representing the primary key
* @return string
*/
public function lastInsertId($table = null, $column = null) {
if ($column && $row = $this->fetch('assoc')) {
return $row[$column];
}
return $this->_driver->lastInsertId($table, $column);
}

}
9 changes: 9 additions & 0 deletions Cake/Database/StatementInterface.php
Expand Up @@ -159,4 +159,13 @@ public function count();
*/
public function bind($params, $types);

/**
* Returns the latest primary inserted using this statement
*
* @param string $table table name or sequence to get last insert value from
* @param string column the name of the column representing the primary key
* @return string
*/
public function lastInsertId($table = null, $column = null);

}
134 changes: 125 additions & 9 deletions Cake/ORM/Entity.php
Expand Up @@ -39,13 +39,29 @@ class Entity implements \ArrayAccess, \JsonSerializable {
*/
protected $_className;

/**
* Holds a list of the properties that were modified or added after this object
* was originally created.
*
* @var array
*/
protected $_dirty = [];

/**
* Holds a cached list of methods that exist in the instanced class
*
* @var array
*/
protected static $_accessors = [];

/**
* Indicates whether or not this entity has already been persisted.
* A null value indicates an unknown persistence status
*
* @var boolean
*/
protected $_persisted = null;

/**
* Initializes the internal properties of this entity out of the
* keys in an array
Expand All @@ -55,12 +71,29 @@ class Entity implements \ArrayAccess, \JsonSerializable {
* ``$entity = new Entity(['id' => 1, 'name' => 'Andrew'])``
*
* @param array $properties hash of properties to set in this entity
* @param boolean $useSetters whether use internal setters for properties or not
* @return void
* @param array $options list of options to use when creating this entity
* the following list of options can be used:
*
* - useSetters: whether use internal setters for properties or not
* - markClean: whether to mark all properties as clean after setting them
* - markNew: whether this instance has not yet been persisted
*/
public function __construct(array $properties = [], $useSetters = true) {
public function __construct(array $properties = [], array $options = []) {
$options += [
'useSetters' => true,
'markClean' => false,
'markNew' => null
];
$this->_className = get_class($this);
$this->set($properties, $useSetters);
$this->set($properties, $options['useSetters']);

if ($options['markClean']) {
$this->clean();
}

if ($options['markNew'] !== null) {
$this->isNew($options['markNew']);
}
}

/**
Expand Down Expand Up @@ -151,12 +184,21 @@ public function set($property, $value = true, $useSetters = true) {
$useSetters = $value;
}

if (!$useSetters) {
$this->_properties = $property + $this->_properties;
return $this;
}

foreach ($property as $p => $value) {
$markDirty = true;
if (isset($this->_properties[$p])) {
$markDirty = $value !== $this->_properties[$p];
}

if ($markDirty) {
$this->dirty($p, true);
}

if (!$useSetters) {
$this->_properties[$p] = $value;
continue;
}

$setter = 'set' . Inflector::camelize($p);
if ($this->_methodExists($setter)) {
$value = $this->{$setter}($value);
Expand Down Expand Up @@ -305,4 +347,78 @@ public function jsonSerialize() {
return $this->_properties;
}

/**
* Returns an array with the requested properties
* stored in this entity, indexed by property name
*
* @param array $properties list of properties to be returned
* @param boolean $onlyDirty Return the requested property only if it is dirty
* @return array
*/
public function extract(array $properties, $onlyDirty = false) {
$result = [];
foreach ($properties as $property) {
if (!$onlyDirty || $this->dirty($property)) {
$result[$property] = $this->get($property);
}
}
return $result;
}

/**
* Sets the dirty status of a single property. If called with no second
* argument, it will return whether the property was modified or not
* after the object creation.
*
* @param string $property the field to set or check status for
* @param null|boolean true means the property was changed, false means
* it was not changed and null will make the function return current state
* for that property
* @return boolean whether the property was changed or not
*/
public function dirty($property, $isDirty = null) {
if ($isDirty === null) {
return isset($this->_dirty[$property]);
}

if (!$isDirty) {
unset($this->_dirty[$property]);
return false;
}

$this->_dirty[$property] = true;
return true;
}

/**
* Sets the entire entity as clean, which means that it will appear as
* no properties being modified or added at all. This is an useful call
* for an initial object hydration
*
* @return void
*/
public function clean() {
$this->_dirty = [];
}

/**
* Returns whether or not this entity has already been persisted.
* This method can return null in the case there is no prior information on
* the status of this entity.
*
* If called with a boolean it will set the known status of this instance,
* true means that the instance is not yet persisted in the database, false
* that it already is.
*
* @param boolean $new true if it is known this instance was persisted
* @return boolean if it is known whether the entity was already persisted
* null otherwise
*/
public function isNew($new = null) {
if ($new === null) {
return $this->_persisted;
}
return $this->_persisted = (bool)$new;
}

}

0 comments on commit e540c1b

Please sign in to comment.