%PDF- %PDF-
Mini Shell

Mini Shell

Direktori : /proc/self/root/home/bitrix/www/bitrix/modules/main/lib/orm/objectify/
Upload File :
Create Path :
Current File : //proc/self/root/home/bitrix/www/bitrix/modules/main/lib/orm/objectify/entityobject.php

<?php
/**
 * Bitrix Framework
 * @package    bitrix
 * @subpackage main
 * @copyright  2001-2018 Bitrix
 */

namespace Bitrix\Main\ORM\Objectify;

use Bitrix\Main\NotImplementedException;
use Bitrix\Main\ORM\Data\DataManager;
use Bitrix\Main\ORM\Entity;
use Bitrix\Main\ORM\Fields\ExpressionField;
use Bitrix\Main\ORM\Fields\IReadable;
use Bitrix\Main\ORM\Fields\Relations\ManyToMany;
use Bitrix\Main\ORM\Fields\Relations\OneToMany;
use Bitrix\Main\ORM\Fields\UserTypeField;
use Bitrix\Main\ORM\Query\Query;
use Bitrix\Main\ORM\Fields\Relations\Reference;
use Bitrix\Main\ORM\Data\Result;
use Bitrix\Main\ORM\Fields\ScalarField;
use Bitrix\Main\ORM\Fields\FieldTypeMask;
use Bitrix\Main\SystemException;
use Bitrix\Main\ArgumentException;

/**
 * Entity object
 *
 * @property-read \Bitrix\Main\ORM\Entity $entity
 * @property-read array $primary
 *
 * @package    bitrix
 * @subpackage main
 */
abstract class EntityObject implements \ArrayAccess
{
	/**
	 * Entity Table class. Read-only property.
	 * @var DataManager
	 */
	static public $dataClass;

	/** @var Entity */
	protected $_entity;

	/**
	 * @var int
	 * @see State
	 */
	protected $_state = State::RAW;

	/**
	 * Actual values fetched from DB and collections of relations
	 * @var mixed[]|static[]|Collection[]
	 */
	protected $_actualValues = [];

	/**
	 * Current values - new or rewritten by setter (except changed collections - they are still in actual values)
	 * @var mixed[]|static[]
	 */
	protected $_currentValues = [];

	/**
	 * Container for non-entity data
	 * @var mixed[]
	 */
	protected $_runtimeValues = [];

	/**
	 * Cache for lastName => LAST_NAME transforming
	 * @var string[]
	 */
	static protected $_camelToSnakeCache = [];

	/**
	 * Cache for LAST_NAME => lastName transforming
	 * @var string[]
	 */
	static protected $_snakeToCamelCache = [];

	final public function __construct($setDefaultValues = true)
	{
		if ($setDefaultValues)
		{
			foreach ($this->entity->getScalarFields() as $fieldName => $field)
			{
				$defaultValue = $field->getDefaultValue($this);

				if ($defaultValue !== null)
				{
					$this->sysSetValue($fieldName, $defaultValue);
				}
			}
		}
	}

	/**
	 * Returns all objects values as an array
	 *
	 * @param int $valuesType
	 * @param int $fieldsMask
	 *
	 * @return array
	 * @throws ArgumentException
	 * @throws SystemException
	 */
	final public function collectValues($valuesType = Values::ALL, $fieldsMask = FieldTypeMask::ALL)
	{
		switch ($valuesType)
		{
			case Values::ACTUAL:
				$objectValues = $this->_actualValues;
				break;
			case Values::CURRENT:
				$objectValues = $this->_currentValues;
				break;
			default:
				$objectValues = array_merge($this->_actualValues, $this->_currentValues);
		}

		// filter with field mask
		if ($fieldsMask !== FieldTypeMask::ALL)
		{
			foreach ($objectValues as $fieldName => $value)
			{
				$fieldMask = $this->entity->getField($fieldName)->getTypeMask();
				if (!($fieldsMask & $fieldMask))
				{
					unset($objectValues[$fieldName]);
				}
			}
		}

		// remap from uppercase to real field names
		$values = [];

		foreach ($objectValues as $k => $v)
		{
			$values[$this->entity->getField($k)->getName()] = $v;
		}

		return $values;
	}

	/**
	 * ActiveRecord save.
	 *
	 * @return Result
	 * @throws ArgumentException
	 * @throws SystemException
	 * @throws \Exception
	 */
	final public function save()
	{
		$result = new Result;
		$dataClass = $this->entity->getDataClass();

		if ($this->_state == State::RAW)
		{
			$data = $this->_currentValues;
			$data['__object'] = $this;

			// put secret key __object to array
			$result = $dataClass::add($data);

			// check for error
			if (!$result->isSuccess())
			{
				return $result;
			}

			// set primary
			foreach ($result->getPrimary() as $primaryName => $primaryValue)
			{
				$this->sysSetActual($primaryName, $primaryValue);
			}
		}
		elseif ($this->_state == State::CHANGED)
		{
			// changed scalar and reference
			if (!empty($this->_currentValues))
			{
				$data = $this->_currentValues;
				$data['__object'] = $this;

				// put secret key __object to array
				$result = $dataClass::update($this->primary, $data);

				// check for error
				if (!$result->isSuccess())
				{
					return $result;
				}
			}
		}
		else
		{
			// nothing to do
			return $result;
		}

		// set other fields, as long as some values could be added or modified in events
		foreach ($result->getData() as $fieldName => $fieldValue)
		{
			$field = $this->entity->getField($fieldName);

			if ($field instanceof ScalarField)
			{
				$fieldValue = $field->cast($fieldValue);
			}

			$this->sysSetActual($fieldName, $fieldValue);
		}

		// changed collections
		$this->sysSaveRelations($result);

		// return if there were errors
		if (!$result->isSuccess())
		{
			return $result;
		}

		// clear current values
		$this->_currentValues = [];

		// change state
		$this->sysChangeState(State::ACTUAL);

		return $result;
	}

	/**
	 * ActiveRecord delete.
	 *
	 * @return Result
	 * @throws ArgumentException
	 * @throws SystemException
	 */
	final public function delete()
	{
		$result = new Result;

		// delete relations
		foreach ($this->entity->getFields() as $field)
		{
			if ($field instanceof OneToMany || $field instanceof ManyToMany)
			{
				$this->sysRemoveAllFromCollection($field->getName());
			}
		}

		$this->sysSaveRelations($result);

		// delete object itself
		$dataClass = static::$dataClass;
		$deleteResult = $dataClass::delete($this->primary);

		if (!$deleteResult->isSuccess())
		{
			$result->addErrors($deleteResult->getErrors());
		}

		// clear status
		foreach ($this->entity->getPrimaryArray()as $primaryName)
		{
			unset($this->_actualValues[$primaryName]);
		}

		$this->sysChangeState(State::RAW);

		return $result;
	}

	/**
	 * Constructs existing object from pre-selected data, including references and relations.
	 *
	 * @param mixed $row Array of [field => value] or single scalar primary value.
	 *
	 * @return static
	 * @throws ArgumentException
	 * @throws SystemException
	 */
	final public static function wakeUp($row)
	{
		/** @var static $objectClass */
		$objectClass = get_called_class();

		/** @var \Bitrix\Main\ORM\Data\DataManager $dataClass */
		$dataClass = static::$dataClass;

		$entity = $dataClass::getEntity();
		$entityPrimary = $entity->getPrimaryArray();

		// normalize input data and primary
		$primary = [];

		if (!is_array($row))
		{
			// it could be single primary
			if (count($entityPrimary) == 1)
			{
				$primary[$entityPrimary[0]] = $row;
				$row = [];
			}
			else
			{
				throw new ArgumentException(sprintf(
					'Multi-primary for %s was not found', $objectClass
				));
			}
		}
		else
		{
			foreach ($entityPrimary as $primaryName)
			{
				if (!isset($row[$primaryName]))
				{
					throw new ArgumentException(sprintf(
						'Primary %s for %s was not found', $primaryName, $objectClass
					));
				}

				$primary[$primaryName] = $row[$primaryName];
				unset($row[$primaryName]);
			}
		}

		// create object
		/** @var static $object */
		$object = new $objectClass(false); // here go with false to not set default values
		$object->sysChangeState(State::ACTUAL);

		// set primary
		foreach ($primary as $primaryName => $primaryValue)
		{
			/** @var ScalarField $primaryField */
			$primaryField = $entity->getField($primaryName);
			$object->sysSetActual($primaryName, $primaryField->cast($primaryValue));
		}

		// set other data
		foreach ($row as $fieldName => $value)
		{
			/** @var ScalarField $primaryField */
			$field = $entity->getField($fieldName);

			if ($field instanceof IReadable)
			{
				$object->sysSetActual($fieldName, $field->cast($value));
			}
			else
			{
				// we have a relation
				if ($value instanceof static || $value instanceof Collection)
				{
					// it is ready data
					$object->sysSetActual($fieldName, $value);
				}
				else
				{
					// wake up relation
					if ($field instanceof Reference)
					{
						// wake up an object
						$remoteObjectClass = $field->getRefEntity()->getObjectClass();
						$remoteObject = $remoteObjectClass::wakeUp($value);

						$object->sysSetActual($fieldName, $remoteObject);
					}
					elseif ($field instanceof OneToMany || $field instanceof ManyToMany)
					{
						// wake up collection
						$remoteCollectionClass = $field->getRefEntity()->getCollectionClass();
						$remoteCollection = $remoteCollectionClass::wakeUp($value);

						$object->sysSetActual($fieldName, $remoteCollection);
					}
				}
			}
		}

		return $object;
	}

	/**
	 * Fills all the values and relations of object
	 *
	 * @param int|string[] $fields Names of fields to fill
	 *
	 * @return mixed
	 * @throws ArgumentException
	 * @throws SystemException
	 */
	final public function fill($fields = FieldTypeMask::ALL)
	{
		// object must have primary
		$primaryFilter = Query::filter();

		foreach ($this->sysRequirePrimary() as $primaryName => $primaryValue)
		{
			$primaryFilter->where($primaryName, $primaryValue);
		}

		// collect fields to be selected
		$fieldsToSelect = $this->entity->getPrimaryArray();

		if (is_array($fields))
		{
			// get custom fields
			$fieldsToSelect = array_merge($fieldsToSelect, $fields);
		}
		elseif (is_scalar($fields) && !is_numeric($fields))
		{
			// one custom field
			$fieldsToSelect[] = $fields;
		}
		else
		{
			// get fields according to selector mask
			$fieldsToSelect = array_merge($fieldsToSelect, $this->sysGetIdleFields($fields));
		}

		// build query
		$dataClass = $this->entity->getDataClass();
		$result = $dataClass::query()->setSelect($fieldsToSelect)->where($primaryFilter)->exec();

		// set object to identityMap of result, and it will be partially completed by fetch
		$im = new IdentityMap;
		$im->put($this);

		$result->setIdentityMap($im);
		$result->fetchObject();

		// set filled flag to collections
		foreach ($fieldsToSelect as $fieldName)
		{
			// check field before continue, it could be remote REF.ID definition so we skip it here
			if ($this->entity->hasField($fieldName))
			{
				$field = $this->entity->getField($fieldName);

				if ($field instanceof OneToMany || $field instanceof ManyToMany)
				{
					/** @var Collection $collection */
					$collection = $this->sysGetValue($fieldName);

					if (empty($collection))
					{
						$collection = $field->getRefEntity()->createCollection();
						$this->_actualValues[$fieldName] = $collection;
					}

					$collection->sysSetFilled();
				}
			}
		}

		// return field value it it was only one
		if (is_array($fields) && count($fields) == 1 && $this->entity->hasField(current($fields)))
		{
			return $this->sysGetValue(current($fields));
		}

		return null;
	}

	/**
	 * Fast popular alternative to __call().
	 *
	 * @return Collection|EntityObject|mixed
	 * @throws SystemException
	 */
	public function getId()
	{
		if (array_key_exists('ID', $this->_currentValues))
		{
			return $this->_currentValues['ID'];
		}
		elseif (array_key_exists('ID', $this->_actualValues))
		{
			return $this->_actualValues['ID'];
		}
		elseif (!$this->entity->hasField('ID'))
		{
			throw new SystemException(sprintf(
				'Unknown method `%s` for object `%s`', 'getId', get_called_class()
			));
		}
		else
		{
			return null;
		}
	}

	/**
	 * @param $fieldName
	 *
	 * @return mixed
	 * @throws ArgumentException
	 * @throws SystemException
	 */
	final public function get($fieldName)
	{
		return $this->__call(__FUNCTION__, func_get_args());
	}

	/**
	 * @param $fieldName
	 *
	 * @return mixed
	 * @throws ArgumentException
	 * @throws SystemException
	 */
	final public function remindActual($fieldName)
	{
		return $this->__call(__FUNCTION__, func_get_args());
	}

	/**
	 * @param $fieldName
	 *
	 * @return mixed
	 * @throws ArgumentException
	 * @throws SystemException
	 */
	/* TODO PHP7 ONLY
	final public function require($fieldName)
	{
		return $this->__call(__FUNCTION__, func_get_args());
	}*/

	/**
	 * @param $fieldName
	 * @param $value
	 *
	 * @return mixed
	 * @throws ArgumentException
	 * @throws SystemException
	 */
	final public function set($fieldName, $value)
	{
		return $this->__call(__FUNCTION__, func_get_args());
	}

	/**
	 * @param $fieldName
	 *
	 * @return mixed
	 * @throws ArgumentException
	 * @throws SystemException
	 */
	final public function reset($fieldName)
	{
		return $this->__call(__FUNCTION__, func_get_args());
	}

	/**
	 * @param $fieldName
	 *
	 * @return mixed
	 * @throws ArgumentException
	 * @throws SystemException
	 */
	/* TODO PHP7 ONLY
	final public function unset($fieldName)
	{
		return $this->__call(__FUNCTION__, func_get_args());
	}*/

	/**
	 * @param $fieldName
	 * @param $value
	 *
	 * @return mixed
	 * @throws ArgumentException
	 * @throws SystemException
	 */
	final public function addTo($fieldName, $value)
	{
		return $this->__call(__FUNCTION__, func_get_args());
	}

	/**
	 * @param $fieldName
	 * @param $value
	 *
	 * @return mixed
	 * @throws ArgumentException
	 * @throws SystemException
	 */
	final public function removeFrom($fieldName, $value)
	{
		return $this->__call(__FUNCTION__, func_get_args());
	}

	/**
	 * @param $fieldName
	 *
	 * @return mixed
	 * @throws ArgumentException
	 * @throws SystemException
	 */
	final public function removeAll($fieldName)
	{
		return $this->__call(__FUNCTION__, func_get_args());
	}

	/**
	 * Magic read-only properties
	 *
	 * @param $name
	 *
	 * @return array|Entity
	 * @throws ArgumentException
	 * @throws SystemException
	 */
	public function __get($name)
	{
		switch ($name)
		{
			case 'entity':
				return $this->sysGetEntity();
			case 'primary':
				return $this->sysGetPrimary();
			case 'dataClass':
				throw new SystemException('Property `dataClass` should be received as static.');
		}

		throw new SystemException(sprintf(
			'Unknown property `%s` for object `%s`', $name, get_called_class()
		));
	}

	/**
	 * Magic read-only properties
	 *
	 * @param $name
	 * @param $value
	 *
	 * @throws SystemException
	 */
	public function __set($name, $value)
	{
		switch ($name)
		{
			case 'entity':
			case 'primary':
			case 'dataClass':
				throw new SystemException(sprintf(
					'Property `%s` for object `%s` is read-only', $name, get_called_class()
				));
		}

		throw new SystemException(sprintf(
			'Unknown property `%s` for object `%s`', $name, get_called_class()
		));
	}

	/**
	 * Magic to handle getters, setters etc.
	 *
	 * @param $name
	 * @param $arguments
	 *
	 * @return mixed
	 * @throws ArgumentException
	 * @throws SystemException
	 */
	public function __call($name, $arguments)
	{
		$first3 = substr($name, 0, 3);

		// regular getter
		if ($first3 == 'get')
		{
			$fieldName = self::sysMethodToFieldCase(substr($name, 3));

			if (!strlen($fieldName))
			{
				$fieldName = strtoupper($arguments[0]);

				// check runtime
				if (array_key_exists($fieldName, $this->_runtimeValues))
				{
					return $this->sysGetRuntime($fieldName);
				}

				// check if custom method exists
				$personalMethodName = $name.static::sysFieldToMethodCase($fieldName);

				if (method_exists($this, $personalMethodName))
				{
					return $this->$personalMethodName(...array_slice($arguments, 1));
				}

				// hard field check
				$this->entity->getField($fieldName);
			}

			// check if field exists
			if ($this->entity->hasField($fieldName))
			{
				return $this->sysGetValue($fieldName);
			}
		}

		// regular setter
		if ($first3 == 'set')
		{
			$fieldName = self::sysMethodToFieldCase(substr($name, 3));
			$value = $arguments[0];

			if (!strlen($fieldName))
			{
				$fieldName = strtoupper($arguments[0]);
				$value = $arguments[1];

				// check for runtime field
				if (array_key_exists($fieldName, $this->_runtimeValues))
				{
					throw new SystemException(sprintf(
						'Setting value for runtime field `%s` in `%s` is not allowed, it is read-only field',
						$fieldName, get_called_class()
					));
				}

				// check if custom method exists
				$personalMethodName = $name.static::sysFieldToMethodCase($fieldName);

				if (method_exists($this, $personalMethodName))
				{
					return $this->$personalMethodName(...array_slice($arguments, 1));
				}

				// hard field check
				$this->entity->getField($fieldName);
			}

			// check if field exists
			if ($this->entity->hasField($fieldName))
			{
				$field = $this->entity->getField($fieldName);

				if ($field instanceof IReadable)
				{
					$value = $field->cast($value);
				}

				return $this->sysSetValue($fieldName, $value);
			}
		}

		$first4 = substr($name, 0, 4);

		// filler
		if ($first4 == 'fill')
		{
			$fieldName = self::sysMethodToFieldCase(substr($name, 4));

			// no custom/personal method for fill

			// check if field exists
			if ($this->entity->hasField($fieldName))
			{
				return $this->fill([$fieldName]);
			}
		}

		$first5 = substr($name, 0, 5);

		// relation adder
		if ($first5 == 'addTo')
		{
			$fieldName = self::sysMethodToFieldCase(substr($name, 5));
			$value = $arguments[0];

			if (!strlen($fieldName))
			{
				$fieldName = strtoupper($arguments[0]);
				$value = $arguments[1];

				// check if custom method exists
				$personalMethodName = $name.static::sysFieldToMethodCase($fieldName);

				if (method_exists($this, $personalMethodName))
				{
					return $this->$personalMethodName(...array_slice($arguments, 1));
				}
			}

			if ($this->entity->hasField($fieldName))
			{
				return $this->sysAddToCollection($fieldName, $value);
			}
		}

		// unsetter
		if ($first5 == 'unset')
		{
			$fieldName = self::sysMethodToFieldCase(substr($name, 5));

			if (!strlen($fieldName))
			{
				$fieldName = strtoupper($arguments[0]);

				// check if custom method exists
				$personalMethodName = $name.static::sysFieldToMethodCase($fieldName);

				if (method_exists($this, $personalMethodName))
				{
					return $this->$personalMethodName(...array_slice($arguments, 1));
				}
			}

			if ($this->entity->hasField($fieldName))
			{
				return $this->sysUnset($fieldName);
			}
		}

		// resetter
		if ($first5 == 'reset')
		{
			$fieldName = self::sysMethodToFieldCase(substr($name, 5));

			if (!strlen($fieldName))
			{
				$fieldName = strtoupper($arguments[0]);

				// check if custom method exists
				$personalMethodName = $name.static::sysFieldToMethodCase($fieldName);

				if (method_exists($this, $personalMethodName))
				{
					return $this->$personalMethodName(...array_slice($arguments, 1));
				}
			}

			if ($this->entity->hasField($fieldName))
			{
				$field = $this->entity->getField($fieldName);

				if ($field instanceof OneToMany || $field instanceof ManyToMany)
				{
					return $this->sysResetRelation($fieldName);
				}
				else
				{
					return $this->sysReset($fieldName);
				}
			}
		}

		$first9 = substr($name, 0, 9);

		// relation mass remover
		if ($first9 == 'removeAll')
		{
			$fieldName = self::sysMethodToFieldCase(substr($name, 9));

			if (!strlen($fieldName))
			{
				$fieldName = strtoupper($arguments[0]);

				// check if custom method exists
				$personalMethodName = $name.static::sysFieldToMethodCase($fieldName);

				if (method_exists($this, $personalMethodName))
				{
					return $this->$personalMethodName(...array_slice($arguments, 1));
				}
			}

			if ($this->entity->hasField($fieldName))
			{
				return $this->sysRemoveAllFromCollection($fieldName);
			}
		}

		$first10 = substr($name, 0, 10);

		// relation remover
		if ($first10 == 'removeFrom')
		{
			$fieldName = self::sysMethodToFieldCase(substr($name, 10));
			$value = $arguments[0];

			if (!strlen($fieldName))
			{
				$fieldName = strtoupper($arguments[0]);
				$value = $arguments[1];

				// check if custom method exists
				$personalMethodName = $name.static::sysFieldToMethodCase($fieldName);

				if (method_exists($this, $personalMethodName))
				{
					return $this->$personalMethodName(...array_slice($arguments, 1));
				}
			}

			if ($this->entity->hasField($fieldName))
			{
				return $this->sysRemoveFromCollection($fieldName, $value);
			}
		}

		$first12 = substr($name, 0, 12);

		// actual value getter
		if ($first12 == 'remindActual')
		{
			$fieldName = self::sysMethodToFieldCase(substr($name, 12));

			if (!strlen($fieldName))
			{
				$fieldName = strtoupper($arguments[0]);

				// check if custom method exists
				$personalMethodName = $name.static::sysFieldToMethodCase($fieldName);

				if (method_exists($this, $personalMethodName))
				{
					return $this->$personalMethodName(...array_slice($arguments, 1));
				}

				// hard field check
				$this->entity->getField($fieldName);
			}

			// check if field exists
			if ($this->entity->hasField($fieldName))
			{
				return $this->_actualValues[$fieldName];
			}
		}

		$first7 = substr($name, 0, 7);

		// strict getter
		if ($first7 == 'require')
		{
			$fieldName = self::sysMethodToFieldCase(substr($name, 7));

			if (!strlen($fieldName))
			{
				$fieldName = strtoupper($arguments[0]);

				// check if custom method exists
				$personalMethodName = $name.static::sysFieldToMethodCase($fieldName);

				if (method_exists($this, $personalMethodName))
				{
					return $this->$personalMethodName(...array_slice($arguments, 1));
				}

				// hard field check
				$this->entity->getField($fieldName);
			}

			// check if field exists
			if ($this->entity->hasField($fieldName))
			{
				return $this->sysGetValue($fieldName, true);
			}
		}

		throw new SystemException(sprintf(
			'Unknown method `%s` for object `%s`', $name, get_called_class()
		));
	}

	/**
	 * @return Entity
	 * @throws \Bitrix\Main\ArgumentException
	 * @throws \Bitrix\Main\SystemException
	 */
	public function sysGetEntity()
	{
		if ($this->_entity === null)
		{
			/** @var \Bitrix\Main\ORM\Data\DataManager $dataClass */
			$dataClass = static::$dataClass;
			$this->_entity = $dataClass::getEntity();
		}

		return $this->_entity;
	}

	/**
	 * Returns [primary => value] array.
	 *
	 * @return array
	 * @throws ArgumentException
	 * @throws SystemException
	 */
	public function sysGetPrimary()
	{
		$primaryValues = [];

		foreach ($this->entity->getPrimaryArray() as $primaryName)
		{
			$primaryValues[$primaryName] = $this->sysGetValue($primaryName);
		}

		return $primaryValues;
	}

	/**
	 * Query Runtime Field values or just any runtime value getter
	 * @internal For internal system usage only.
	 *
	 * @param $name
	 *
	 * @return mixed
	 */
	public function sysGetRuntime($name)
	{
		return $this->_runtimeValues[$name];
	}

	/**
	 * Any runtime value setter
	 * @internal For internal system usage only.
	 *
	 * @param $name
	 * @param $value
	 *
	 * @return $this
	 */
	public function sysSetRuntime($name, $value)
	{
		$this->_runtimeValues[$name] = $value;

		return $this;
	}

	/**
	 * Sets actual value.
	 * @internal For internal system usage only.
	 *
	 * @param $fieldName
	 * @param $value
	 */
	public function sysSetActual($fieldName, $value)
	{
		$this->_actualValues[strtoupper($fieldName)] = $value;
	}

	/**
	 * Changes state.
	 * @see State
	 * @internal For internal system usage only.
	 *
	 * @param $state
	 */
	public function sysChangeState($state)
	{
		if ($this->_state !== $state)
		{
			/* not sure if we need check or changes here
			if ($state == State::RAW)
			{
				// actual should be empty
			}
			elseif ($state == State::ACTUAL)
			{
				// runtime values should be empty
			}
			elseif ($state == State::CHANGED)
			{
				// runtime values should not be empty
			}*/

			$this->_state = $state;
		}

	}

	/**
	 * Returns current state.
	 * @see State
	 * @internal For internal system usage only.
	 *
	 * @return int
	 */
	public function sysGetState()
	{
		return $this->_state;
	}

	/**
	 * Regular getter, called by __call.
	 * @internal For internal system usage only.
	 *
	 * @param string $fieldName
	 * @param bool $require Throws an exception in the absence of value
	 *
	 * @return mixed
	 * @throws SystemException
	 */
	public function sysGetValue($fieldName, $require = false)
	{
		$fieldName = strtoupper($fieldName);

		if (array_key_exists($fieldName, $this->_currentValues))
		{
			return $this->_currentValues[$fieldName];
		}
		else
		{
			if ($require && !array_key_exists($fieldName, $this->_actualValues))
			{
				throw new SystemException(sprintf(
					'%s value is required for further operations', $fieldName
				));
			}

			return $this->_actualValues[$fieldName];
		}
	}

	/**
	 * Regular setter, called by __call. Doesn't validate values.
	 * @internal For internal system usage only.
	 *
	 * @param $fieldName
	 * @param $value
	 *
	 * @return $this
	 * @throws ArgumentException
	 * @throws SystemException
	 */
	public function sysSetValue($fieldName, $value)
	{
		$fieldName = strtoupper($fieldName);
		$field = $this->entity->getField($fieldName);

		// system validations
		if ($field instanceof ScalarField)
		{
			// restrict updating primary
			if ($this->_state !== State::RAW && in_array($field->getName(), $this->entity->getPrimaryArray()))
			{
				throw new SystemException(sprintf(
					'Setting value for Primary `%s` in `%s` is not allowed, it is read-only field',
					$field->getName(), get_called_class()
				));
			}
		}

		// no setter for expressions
		if ($field instanceof ExpressionField && !($field instanceof UserTypeField))
		{
			throw new SystemException(sprintf(
				'Setting value for ExpressionField `%s` in `%s` is not allowed, it is read-only field',
				$fieldName, get_called_class()
			));
		}

		if ($field instanceof Reference)
		{
			if (!empty($value))
			{
				// validate object class and skip null
				$remoteObjectClass = $field->getRefEntity()->getObjectClass();

				if (!($value instanceof $remoteObjectClass))
				{
					throw new ArgumentException(sprintf(
						'Expected instance of `%s`, got `%s` instead', $remoteObjectClass, get_class($value)
					));
				}
			}
		}

		// change only if value is different from actual
		if (array_key_exists($fieldName, $this->_actualValues))
		{
			if ($field instanceof IReadable)
			{
				if ($field->cast($value) === $this->_actualValues[$fieldName])
				{
					// forget previous runtime change
					unset($this->_currentValues[$fieldName]);
					return $this;
				}
			}
			elseif ($field instanceof Reference)
			{
				/** @var static $value */
				if ($value->primary === $this->_actualValues[$fieldName]->primary)
				{
					// forget previous runtime change
					unset($this->_currentValues[$fieldName]);
					return $this;
				}
			}
		}

		// set value
		if ($field instanceof ScalarField || $field instanceof UserTypeField)
		{
			$this->_currentValues[$fieldName] = $value;
		}
		elseif ($field instanceof Reference)
		{
			/** @var static $value */
			$this->_currentValues[$fieldName] = $value;

			// set elemental fields if there are any
			$elementals = $field->getElementals();

			if (!empty($elementals))
			{
				foreach ($elementals as $localFieldName => $remoteFieldName)
				{
					$elementalValue = empty($value) ? null : $value->sysGetValue($remoteFieldName);
					$this->sysSetValue($localFieldName, $elementalValue);
				}
			}
		}
		else
		{
			throw new SystemException(sprintf(
				'Unknown field type `%s` in system setter of `%s`', get_class($field), get_called_class()
			));
		}

		if ($this->_state == State::ACTUAL)
		{
			$this->sysChangeState(State::CHANGED);
		}

		return $this;
	}

	/**
	 * @internal For internal system usage only.
	 *
	 * @param $fieldName
	 *
	 * @return $this
	 */
	public function sysUnset($fieldName)
	{
		$fieldName = strtoupper($fieldName);

		unset($this->_currentValues[$fieldName]);
		unset($this->_actualValues[$fieldName]);

		return $this;
	}

	/**
	 * @internal For internal system usage only.
	 *
	 * @param $fieldName
	 *
	 * @return $this
	 */
	public function sysReset($fieldName)
	{
		$fieldName = strtoupper($fieldName);

		unset($this->_currentValues[$fieldName]);

		return $this;
	}

	/**
	 * @internal For internal system usage only.
	 *
	 * @param $fieldName
	 *
	 * @return $this
	 */
	public function sysResetRelation($fieldName)
	{
		$fieldName = strtoupper($fieldName);

		if (isset($this->_actualValues[$fieldName]))
		{
			/** @var Collection $collection */
			$collection = $this->_actualValues[$fieldName];
			$collection->sysResetChanges(true);
		}

		return $this;
	}

	/**
	 * @internal For internal system usage only.
	 *
	 * @return array
	 * @throws ArgumentException
	 * @throws SystemException
	 */
	public function sysRequirePrimary()
	{
		$primaryValues = [];

		foreach ($this->entity->getPrimaryArray() as $primaryName)
		{
			try
			{
				$primaryValues[$primaryName] = $this->sysGetValue($primaryName, true);
			}
			catch (SystemException $e)
			{
				throw new SystemException(sprintf(
					'Primary `%s` value is required for further operations', $primaryName
				));
			}
		}

		return $primaryValues;
	}

	/**
	 * @internal For internal system usage only.
	 *
	 * @param int $mask
	 *
	 * @return array
	 * @throws ArgumentException
	 * @throws SystemException
	 */
	public function sysGetIdleFields($mask = FieldTypeMask::ALL)
	{
		$list = [];

		foreach ($this->entity->getFields() as $field)
		{
			$fieldMask = $field->getTypeMask();

			if (!isset($this->_actualValues[strtoupper($field->getName())])
				&& ($mask & $fieldMask)
			)
			{
				$list[] = $field->getName();
			}
		}

		return $list;
	}

	/**
	 * @internal For internal system usage only.
	 *
	 * @param Result $result
	 *
	 * @throws ArgumentException
	 * @throws SystemException
	 */
	protected function sysSaveRelations(Result $result)
	{
		foreach ($this->_actualValues as $fieldName => $value)
		{
			$field = $this->entity->getField($fieldName);

			if ($field instanceof OneToMany && $value->sysIsChanged())
			{
				// save changed elements of collection
				$collection = $value;

				foreach ($collection->sysGetChanges() as $change)
				{
					list($remoteObject,) = $change;

					// no matter what changeType is, just save the remote object
					// elementals will be changed after add or nulled after remove
					/** @var static $remoteObject */
					$remoteResult = $remoteObject->save();

					if (!$remoteResult->isSuccess())
					{
						$result->addErrors($remoteResult->getErrors());
					}
				}

				// forget collection changes
				$collection->sysResetChanges();
			}
			elseif ($field instanceof ManyToMany && $value->sysIsChanged())
			{
				$collection = $value;

				foreach ($collection->sysGetChanges() as $change)
				{
					list($remoteObject, $changeType) = $change;

					// initialize mediator object
					$mediatorObjectClass = $field->getMediatorEntity()->getObjectClass();
					$localReferenceName = $field->getLocalReferenceName();
					$remoteReferenceName = $field->getRemoteReferenceName();

					/** @var static $mediatorObject */
					$mediatorObject = new $mediatorObjectClass;
					$mediatorObject->sysSetValue($localReferenceName, $this);
					$mediatorObject->sysSetValue($remoteReferenceName, $remoteObject);

					// add or remove mediator depending on changeType
					if ($changeType == Collection::OBJECT_ADDED)
					{
						$mediatorObject->save();
					}
					elseif ($changeType == Collection::OBJECT_REMOVED)
					{
						// destroy directly through data class
						$mediatorDataClass = $field->getMediatorEntity()->getDataClass();
						$mediatorDataClass::delete($mediatorObject->primary);
					}
				}

				// forget collection changes
				$collection->sysResetChanges();
			}
		}
	}

	/**
	 * @internal For internal system usage only.
	 *
	 * @param $fieldName
	 * @param $remoteObject
	 *
	 * @throws ArgumentException
	 * @throws SystemException
	 */
	public function sysAddToCollection($fieldName, $remoteObject)
	{
		$fieldName = strtoupper($fieldName);

		/** @var OneToMany $field */
		$field = $this->entity->getField($fieldName);
		$remoteObjectClass = $field->getRefEntity()->getObjectClass();

		// validate object class
		if (!($remoteObject instanceof $remoteObjectClass))
		{
			throw new ArgumentException(sprintf(
				'Expected instance of `%s`, got `%s` instead', $remoteObjectClass, get_class($remoteObject)
			));
		}

		// initialize collection
		$collection = $this->sysGetValue($fieldName);

		if (empty($collection))
		{
			$collection = $field->getRefEntity()->createCollection();
			$this->_actualValues[$fieldName] = $collection;
		}

		// add to collection
		$collection->add($remoteObject);

		if ($field instanceof OneToMany)
		{
			// set self to the object
			$remoteFieldName = $field->getRefField()->getName();
			$remoteObject->sysSetValue($remoteFieldName, $this);
		}

		// mark object as changed
		if ($this->_state == State::ACTUAL)
		{
			$this->sysChangeState(State::CHANGED);
		}
	}

	/**
	 * @internal For internal system usage only.
	 *
	 * @param $fieldName
	 * @param $remoteObject
	 *
	 * @throws ArgumentException
	 * @throws SystemException
	 */
	public function sysRemoveFromCollection($fieldName, $remoteObject)
	{
		$fieldName = strtoupper($fieldName);

		/** @var OneToMany $field */
		$field = $this->entity->getField($fieldName);
		$remoteObjectClass = $field->getRefEntity()->getObjectClass();

		// validate object class
		if (!($remoteObject instanceof $remoteObjectClass))
		{
			throw new ArgumentException(sprintf(
				'Expected instance of `%s`, got `%s` instead', $remoteObjectClass, get_class($remoteObject)
			));
		}

		/** @var Collection $collection Initialize collection */
		$collection = $this->sysGetValue($fieldName);

		if (empty($collection))
		{
			$collection = $field->getRefEntity()->createCollection();
			$this->_actualValues[$fieldName] = $collection;
		}

		// remove from collection
		$collection->remove($remoteObject);

		if ($field instanceof OneToMany)
		{
			// remove self from the object
			$remoteFieldName = $field->getRefField()->getName();
			$remoteObject->sysSetValue($remoteFieldName, null);
		}

		// mark object as changed
		if ($this->_state == State::ACTUAL)
		{
			$this->sysChangeState(State::CHANGED);
		}
	}

	/**
	 * @internal For internal system usage only.
	 *
	 * @param $fieldName
	 *
	 * @throws ArgumentException
	 * @throws SystemException
	 */
	public function sysRemoveAllFromCollection($fieldName)
	{
		$fieldName = strtoupper($fieldName);

		/** @var OneToMany|ManyToMany $field */
		$field = $this->entity->getField($fieldName);

		/** @var Collection $collection initialize collection */
		$collection = $this->sysGetValue($fieldName);

		if (empty($collection))
		{
			$collection = $field->getRefEntity()->createCollection();
			$this->_actualValues[$fieldName] = $collection;
		}

		// check collection fullness
		if (!$collection->sysIsFilled())
		{
			// we need only primary here
			$remotePrimaryDefinitions = [];

			foreach ($field->getRefEntity()->getPrimaryArray() as $primaryName)
			{
				$remotePrimaryDefinitions[] = $fieldName.'.'.$primaryName;
			}

			$this->fill($remotePrimaryDefinitions);

			// we can set fullness flag here
			$collection->sysSetFilled();
		}

		// remove one by one
		foreach ($collection as $remoteObject)
		{
			$this->sysRemoveFromCollection($fieldName, $remoteObject);
		}
	}

	/**
	 * @internal For internal system usage only.
	 *
	 * @param $methodName
	 *
	 * @return string
	 */
	public static function sysMethodToFieldCase($methodName)
	{
		if (!isset(static::$_camelToSnakeCache[$methodName]))
		{
			static::$_camelToSnakeCache[$methodName] = strtoupper(
				preg_replace('/(.)([A-Z])/', '$1_$2', $methodName)
			);
		}

		return static::$_camelToSnakeCache[$methodName];
	}

	/**
	 * @internal For internal system usage only.
	 *
	 * @param $fieldName
	 *
	 * @return string
	 */
	public static function sysFieldToMethodCase($fieldName)
	{
		if (!isset(static::$_snakeToCamelCache[$fieldName]))
		{
			static::$_snakeToCamelCache[$fieldName] = str_replace(' ', '', ucwords(
					str_replace('_', ' ', strtolower($fieldName))
			));
		}

		return static::$_snakeToCamelCache[$fieldName];
	}

	/**
	 * ArrayAccess interface implementation.
	 *
	 * @param mixed $offset
	 *
	 * @return bool
	 * @throws ArgumentException
	 * @throws SystemException
	 */
	public function offsetExists($offset)
	{
		return $this->entity->hasField($offset);
	}

	/**
	 * ArrayAccess interface implementation.
	 *
	 * @param mixed $offset
	 *
	 * @return mixed|null
	 * @throws ArgumentException
	 * @throws SystemException
	 */
	public function offsetGet($offset)
	{
		if ($this->offsetExists($offset))
		{
			// regular field
			return $this->get($offset);
		}
		elseif (array_key_exists($offset, $this->_runtimeValues))
		{
			// runtime field
			return $this->sysGetRuntime($offset);
		}

		return $this->offsetExists($offset) ? $this->get($offset) : null;
	}

	/**
	 * ArrayAccess interface implementation.
	 *
	 * @param mixed $offset
	 * @param mixed $value
	 *
	 * @throws ArgumentException
	 * @throws SystemException
	 */
	public function offsetSet($offset, $value)
	{
		if (is_null($offset))
		{
			throw new ArgumentException('Field name should be set');
		}
		else
		{
			$this->set($offset, $value);
		}
	}

	/**
	 * ArrayAccess interface implementation.
	 *
	 * @param mixed $offset
	 */
	public function offsetUnset($offset)
	{
		$this->unset($offset);
	}
}

Zerion Mini Shell 1.0