Created
November 9, 2016 12:44
-
-
Save thinkingmedia/8e0f39a72dfdea8ca51ced987c13764c to your computer and use it in GitHub Desktop.
A handler for ORM read/write operations
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Gems\Tools; | |
use Cake\Datasource\EntityInterface; | |
use Cake\Error\PHP7ErrorException; | |
use Cake\Log\Log; | |
use Cake\ORM\Table; | |
use Cake\Utility\Hash; | |
use Error; | |
use Gems\Exceptions\GemsArgumentException; | |
use Gems\Exceptions\GemsEntityException; | |
use Gems\Exceptions\GemsTransactionException; | |
/** | |
* Generic assert handlers. | |
*/ | |
class GemsAssert | |
{ | |
/** | |
* If there is a transaction in progress. | |
* | |
* @var bool | |
* @todo This won't work with multi-threading (future) | |
*/ | |
private static $in_transaction = false; | |
/** | |
* Performs a database write operation and throws an exception if save fails. | |
* | |
* @param Table $table | |
* @param EntityInterface $entity | |
* @param array $options | |
* @throws GemsEntityException | |
*/ | |
private static function _save(Table $table, EntityInterface $entity, array $options) | |
{ | |
$create = $entity->isNew(); | |
if (!$table->save($entity, $options)) { | |
$name = $table->alias(); | |
$op = $create ? 'INSERT' : 'UPDATE'; | |
throw new GemsEntityException("{$name} could not {$op} entity {$entity->id}", $entity); | |
} | |
} | |
/** | |
* Attempts to save changes to the table. If there is a MySQL deadlock error the save operation is repeated. | |
* | |
* @param Table $table | |
* @param EntityInterface $entity | |
* @param array $options | |
* @throws GemsEntityException | |
*/ | |
private static function retrySave(Table $table, EntityInterface $entity, array $options = []) | |
{ | |
$options = $options + ['atomic' => true]; | |
if ($options['atomic'] === false) { | |
self::_save($table, $entity, $options); | |
} else { | |
self::transaction($table, function () use ($table, $entity, $options) { | |
self::_save($table, $entity, $options); | |
}); | |
} | |
} | |
/** | |
* Throws an exception if the entity fails to be created. | |
* | |
* @param Table $table | |
* @param array $data | |
* @param null $callable | |
* @param array $options | |
* @return EntityInterface | |
* @throws GemsEntityException | |
*/ | |
public static function create(Table $table, array $data, callable $callable = null, $options = []) | |
{ | |
$result = true; | |
try { | |
$entity = $table->newEntity($data); | |
$result = $callable === null | |
? true | |
: call_user_func($callable, $entity); | |
} finally { | |
if ($result !== false) { | |
self::retrySave($table, $entity, $options); | |
} | |
} | |
return $result !== false | |
? $entity | |
: null; | |
} | |
/** | |
* @param Table $table | |
* @param EntityInterface $entity | |
* @param callable|null $callable | |
* @param array $options | |
* @throws GemsEntityException | |
*/ | |
public static function delete(Table $table, EntityInterface $entity, callable $callable = null, $options = []) | |
{ | |
$result = true; | |
try { | |
$result = $callable === null | |
? true | |
: call_user_func($callable, $entity); | |
} finally { | |
if (!$table->delete($entity, $options)) { | |
$name = $table->alias(); | |
throw new GemsEntityException("{$name} could not delete entity.", $entity); | |
} | |
} | |
} | |
/** | |
* Returns a flat array describing all the errors in an entity. | |
* | |
* @param EntityInterface $entity | |
* @param string $format | |
* @return array | |
*/ | |
public static function errors(EntityInterface $entity, $format = "%s: %s => %s") | |
{ | |
$errors = []; | |
foreach (Hash::flatten($entity->errors(), '.') as $field => $message) { | |
$parts = explode('.', $field); | |
$ruleName = array_pop($parts); | |
$errors[] = sprintf($format, implode('.', $parts), $ruleName, $message); | |
} | |
return $errors; | |
} | |
/** | |
* Logs the errors of an entity. | |
* | |
* @param EntityInterface $entity | |
* @param int $indent | |
* @param string $format | |
*/ | |
public static function logErrors(EntityInterface $entity, $indent = 3, $format = "%s: %s => %s") | |
{ | |
foreach (self::errors($entity, $format) as $line) { | |
Log::error(GemsStrings::leftPad($line, $indent)); | |
} | |
} | |
/** | |
* Throws an exception if the value is empty. | |
* | |
* @param mixed $value | |
* @param string $msg | |
* @return mixed | |
* @throws GemsArgumentException | |
*/ | |
public static function notEmpty($value, $msg) | |
{ | |
if (empty($value)) { | |
throw new GemsArgumentException($msg); | |
} | |
return $value; | |
} | |
/** | |
* Throws an exception if the entity fails to be saved. | |
* | |
* @param Table $table | |
* @param EntityInterface $entity | |
* @param array $options | |
* @param callable $callable | |
* @return EntityInterface|null | |
* @throws GemsEntityException | |
*/ | |
public static function save(Table $table, EntityInterface $entity, callable $callable = null, $options = []) | |
{ | |
$result = true; | |
try { | |
$result = $callable === null | |
? true | |
: $callable($entity); | |
} finally { | |
if ($entity->dirty() && $result !== false) { | |
self::retrySave($table, $entity, $options); | |
} | |
} | |
return $result !== false | |
? $entity | |
: null; | |
} | |
/** | |
* Executes the callback inside a transaction, and will repeat the callback if the transaction | |
* fails because of deadlock. | |
* | |
* @param Table $table | |
* @param callable $callable | |
* @return mixed | |
* @throws GemsTransactionException | |
* @throws \Exception | |
* @todo Can this be moved to a custom Connection that implements a retry on transactional? | |
*/ | |
public static function transaction(Table $table, callable $callable) | |
{ | |
if (self::$in_transaction === true) { | |
return $callable($table); | |
} | |
for ($retry = 0; $retry < 10; $retry++) { | |
try { | |
self::$in_transaction = true; | |
try { | |
return $table->connection()->transactional(function () use ($table, $callable) { | |
try { | |
return $callable($table); | |
} catch (Error $error) { | |
// fixes bug in Cake where it only catches Exception types. | |
// convert Error to Exception | |
throw new PHP7ErrorException($error); | |
} | |
}); | |
} finally { | |
self::$in_transaction = false; | |
} | |
} catch (\PDOException $ex) { | |
if (GemsStrings::contains($ex->getMessage(), 'SQLSTATE[40001]')) { | |
Log::error($ex->getMessage()); | |
sleep(100); | |
continue; | |
} | |
throw $ex; | |
} | |
} | |
throw new GemsTransactionException(sprintf("Failure to update [%s] increase retry count.", $table->alias())); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment