<?php
namespace Cms\CoreBundle\Model;
/**
* Class Context
* @package Cms\CoreBundle\Model
*/
abstract class Context
{
const LOCKED = true;
const UNLOCKED = false;
/**
* Flag for whether or not the context is locked.
*
* @var bool
*/
private $locked = true;
/**
* Holds context data.
*
* @var array
*/
private $data = [];
/**
* Determines the usable keys in the context.
* Also designates whether or not they are protected.
*
* @var array
*/
private $keys = [];
/**
* @param array $keys
*/
public function __construct(array $keys)
{
$this->keys = $keys;
}
/**
* Locks protected keys to keep them from being written to.
*
* @return $this
*/
public function lock()
{
$this->locked = true;
return $this;
}
/**
* Unlocks protected keys to allow them to be written to.
*
* @return $this
*/
public function unlock()
{
$this->locked = false;
return $this;
}
/**
* @param array $keys
* @return $this
*/
public function reset(array $keys = null)
{
if ($keys === null) {
$keys = array_keys($this->keys);
}
foreach ($keys as $key) {
$this->clear($key);
}
return $this;
}
/**
* @param callable $callable
* @return $this
* @throws \Exception
*/
public function transactional(callable $callable)
{
$copy = $this->data;
try {
$this->unlock();
$result = $callable($this);
} catch(\Exception $e) {
$result = $e;
} finally {
$this->lock();
}
$this->data = $copy;
if ($result instanceof \Exception) {
throw $result;
}
return $result;
}
/**
* Allows for a transactional attempt at writing to a protected key.
* It will always lock when finished.
* Using this method is preferred to manual locking/unlocking.
*
* @param callable $callable
* @return $this
*/
public function escort(callable $callable)
{
try {
$this->unlock();
$callable($this);
} finally {
$this->lock();
}
return $this;
}
/**
* Gets the value for the given key.
*
* @param string $key
* @return mixed
*/
private function get($key)
{
return $this->allocate($key)->data[$key];
}
/**
* Setter that protects a value from being written to more than once.
* The context can be "unlocked" to allow writing to these protected properties if needed.
*
* @param string $key
* @param mixed $value
* @return $this
* @throws \Exception
*/
protected function set($key, $value)
{
if ($this->locked && $this->isProtected($key) && ! $this->isEmpty($key)) {
throw new \Exception();
}
$this->data[$key] = $value;
return $this;
}
/**
* Forces a value to be cleared (set to null).
* Respects protection.
*
* @param string $key
* @return $this
* @throws \Exception
*/
private function clear($key)
{
if ($this->isEmpty($key)) {
return $this;
}
return $this->set($key, null);
}
/**
* Makes sure the given key exists in the data holder.
*
* @param string $key
* @return $this
*/
private function allocate($key)
{
if ( ! isset($this->data[$key])) {
$this->data[$key] = null;
}
return $this;
}
/**
* Determines whether or not the given key is protected.
*
* @param string $key
* @return bool
* @throws \Exception
*/
private function isProtected($key)
{
if ( ! isset($this->keys[$key])) {
throw new \Exception();
}
return ($this->keys[$key] === true);
}
/**
* Determines whether or not the value for the given key is empty/null.
*
* @param string $key
* @return bool
*/
private function isEmpty($key)
{
return ($this->allocate($key)->data[$key] === null);
}
/**
* @param string $name
* @param array $arguments
* @return mixed
* @throws \Exception
*/
public function __call($name, array $arguments)
{
if (preg_match('/^(get|set|clear)(.+)$/', $name, $matches) === 1) {
$method = $matches[1];
$key = lcfirst($matches[2]);
if ( ! isset($this->keys[$key])) {
throw new \Exception();
}
switch ($method) {
case 'get':
return $this->get($key);
case 'set':
if (count($arguments) !== 1) {
throw new \Exception();
}
return $this->set($key, $arguments[0]);
case 'clear':
return $this->clear($key);
}
}
throw new \Exception();
}
/**
* @param string $name
* @return mixed
*/
public function __get($name)
{
return $this->get($name);
}
/**
* @param string $name
* @return bool
*/
public function __isset($name)
{
return array_key_exists($name, $this->data);
}
}