code:
<?php

/**
 * This file is part of the Nextras\Orm library.
 * @license    MIT
 * @link       https://github.com/nextras/orm
 */

namespace Nextras\Orm\Entity;

use Nextras\Orm\Collection\IEntityPreloadContainer;
use Nextras\Orm\Entity\Reflection\EntityMetadata;
use Nextras\Orm\Entity\Reflection\PropertyMetadata;
use Nextras\Orm\InvalidArgumentException;
use Nextras\Orm\InvalidStateException;
use Nextras\Orm\Model\MetadataStorage;
use Nextras\Orm\Relationships\IRelationshipCollection;
use Nextras\Orm\Relationships\IRelationshipContainer;
use Nextras\Orm\Repository\IRepository;

abstract class AbstractEntity implements IEntity
{
	/** @var EntityMetadata */
	protected $metadata;

	/** @var IRepository */
	private $repository;

	/** @var array */
	private $data = [];

	/** @var array */
	private $validated = [];

	/** @var array */
	private $modified = [];

	/** @var mixed */
	private $persistedId = null;

	/** @var IEntityPreloadContainer */
	private $preloadContainer;

	public function __construct()
	{
		$this->modified[null] = true;
		$this->metadata = $this->createMetadata();
		$this->fireEvent('onCreate');
	}

	public function fireEvent($method, $args = [])
	{
		call_user_func_array([$this, $method], $args);
	}

	public function getModel($need = true)
	{
		$repository = $this->getRepository($need);
		return $repository ? $repository->getModel($need) : null;
	}

	public function getRepository($need = true)
	{
		if ($this->repository === null && $need) {
			throw new InvalidStateException('Entity is not attached to repository.');
		}
		return $this->repository;
	}

	public function isAttached()
	{
		return $this->repository !== null;
	}

	public function getMetadata()
	{
		return $this->metadata;
	}

	public function isModified($name = null)
	{
		if ($name === null) {
			return (bool) $this->modified;
		}

		$this->metadata->getProperty($name); // checks property existence
		return isset($this->modified[null]) || isset($this->modified[$name]);
	}

	public function setAsModified($name = null)
	{
		$this->modified[$name] = true;
		return $this;
	}

	public function isPersisted()
	{
		return $this->persistedId !== null;
	}

	public function getPersistedId()
	{
		return $this->persistedId;
	}

	public function setPreloadContainer(IEntityPreloadContainer $overIterator = null)
	{
		$this->preloadContainer = $overIterator;
		return $this;
	}

	public function getPreloadContainer()
	{
		return $this->preloadContainer;
	}

	public function setValue($name, $value)
	{
		$metadata = $this->metadata->getProperty($name);
		if ($metadata->isReadonly) {
			throw new InvalidArgumentException("Property '$name' is read-only.");
		}

		$this->internalSetValue($metadata, $name, $value);
		return $this;
	}

	public function setReadOnlyValue($name, $value)
	{
		$metadata = $this->metadata->getProperty($name);
		$this->internalSetValue($metadata, $name, $value);
		return $this;
	}

	/**
	 * Returns value.
	 * @param  string   $name
	 * @return mixed
	 */
	public function &getValue($name)
	{
		$property = $this->metadata->getProperty($name);
		return $this->internalGetValue($property, $name);
	}

	public function hasValue($name)
	{
		if (!$this->metadata->hasProperty($name)) {
			return false;
		}

		return $this->internalHasValue($this->metadata->getProperty($name), $name);
	}

	public function setRawValue($name, $value)
	{
		$property = $this->metadata->getProperty($name);
		if ($property->isVirtual) {
			$this->internalSetValue($property, $name, $value);
			return;
		}

		if (isset($this->data[$name]) && $this->data[$name] instanceof IProperty) {
			$this->data[$name]->setRawValue($value);
		} else {
			$this->data[$name] = $value;
			$this->modified[$name] = true;
			$this->validated[$name] = false;
		}
	}

	public function &getRawValue($name)
	{
		$property = $this->metadata->getProperty($name);
		if ($property->isVirtual) {
			$value = $this->internalGetValue($property, $name);
			return $value;
		}

		if (!isset($this->validated[$name])) {
			$this->initProperty($property, $name);
		}

		$value = $this->data[$name];
		if ($value instanceof IProperty) {
			$value = $value->getRawValue();
		}
		return $value;
	}

	public function getProperty($name)
	{
		$propertyMetadata = $this->metadata->getProperty($name);
		if (!isset($this->validated[$name])) {
			$this->initProperty($propertyMetadata, $name);
		}

		return $this->data[$name];
	}

	public function getRawProperty($name)
	{
		$this->metadata->getProperty($name);
		return isset($this->data[$name]) ? $this->data[$name] : null;
	}

	public function toArray($mode = self::TO_ARRAY_RELATIONSHIP_AS_IS)
	{
		return ToArrayConverter::toArray($this, $mode);
	}

	public function __clone()
	{
		$id = $this->hasValue('id') ? $this->getValue('id') : null;
		$persistedId = $this->persistedId;
		foreach ($this->getMetadata()->getProperties() as $name => $metadataProperty) {
			if ($metadataProperty->isVirtual) {
				continue;
			}

			// getValue loads data & checks for not null values
			if ($this->hasValue($name) && is_object($this->data[$name])) {
				if ($this->data[$name] instanceof IRelationshipCollection) {
					$data = iterator_to_array($this->data[$name]->get());
					$this->data['id'] = null;
					$this->persistedId = null;
					$this->data[$name] = clone $this->data[$name];
					$this->data[$name]->setParent($this);
					$this->data[$name]->set($data);
					$this->data['id'] = $id;
					$this->persistedId = $persistedId;

				} elseif ($this->data[$name] instanceof IRelationshipContainer) {
					$this->data[$name] = clone $this->data[$name];
					$this->data[$name]->setParent($this);

				} else {
					$this->data[$name] = clone $this->data[$name];
				}
			}
		}
		$this->data['id'] = null;
		$this->persistedId = null;
		$this->modified[null] = true;
		$this->preloadContainer = null;

		if ($repository = $this->repository) {
			$this->repository = null;
			$repository->attach($this);
		}
	}

	public function serialize()
	{
		return [
			'modified' => $this->modified,
			'validated' => $this->validated,
			'data' => $this->toArray(IEntity::TO_ARRAY_RELATIONSHIP_AS_ID),
			'persistedId' => $this->persistedId,
		];
	}

	public function unserialize($unserialized)
	{
		$this->persistedId = $unserialized['persistedId'];
		$this->modified = $unserialized['modified'];
		$this->validated = $unserialized['validated'];
		$this->data = $unserialized['data'];
	}

	// === events ======================================================================================================

	protected function onCreate()
	{
	}

	protected function onLoad(array $data)
	{
		foreach ($this->metadata->getProperties() as $name => $metadataProperty) {
			if (!$metadataProperty->isVirtual && isset($data[$name])) {
				$this->data[$name] = $data[$name];
			}
		}

		$this->persistedId = $this->getValue('id');
	}

	protected function onRefresh(array $data)
	{
		foreach ($this->metadata->getProperties() as $name => $metadataProperty) {
			if (!$metadataProperty->isVirtual && isset($data[$name])) {
				$this->internalSetValue($metadataProperty, $name, $data[$name]);
			}
		}
	}

	protected function onFree()
	{
		$this->data = [];
		$this->persistedId = null;
		$this->validated = [];
	}

	protected function onAttach(IRepository $repository, EntityMetadata $metadata)
	{
		$this->attach($repository);
		$this->metadata = $metadata;
	}

	protected function onDetach()
	{
		$this->repository = null;
	}

	protected function onPersist($id)
	{
		// $id property may be marked as read-only
		$this->setReadOnlyValue('id', $id);
		$this->persistedId = $this->getValue('id');
		$this->modified = [];
	}

	protected function onBeforePersist()
	{
	}

	protected function onAfterPersist()
	{
	}

	protected function onBeforeInsert()
	{
	}

	protected function onAfterInsert()
	{
	}

	protected function onBeforeUpdate()
	{
	}

	protected function onAfterUpdate()
	{
	}

	protected function onBeforeRemove()
	{
	}

	protected function onAfterRemove()
	{
		$this->repository = null;
		$this->persistedId = null;
		$this->modified = [];
	}

	// === internal implementation =====================================================================================

	/**
	 * @return EntityMetadata
	 */
	protected function createMetadata()
	{
		return MetadataStorage::get(get_class($this));
	}

	private function setterPrimaryProxy($value, PropertyMetadata $metadata)
	{
		$keys = $this->metadata->getPrimaryKey();
		if (!$metadata->isVirtual) {
			return $value;
		}

		if (count($keys) !== count($value)) {
			$class = get_class($this);
			throw new InvalidStateException("Value for $class::\$id has insufficient number of parameters.");
		}

		$value = (array) $value;
		foreach ($keys as $key) {
			$this->setRawValue($key, array_shift($value));
		}
		return IEntity::SKIP_SET_VALUE;
	}

	private function getterPrimaryProxy($value = null, PropertyMetadata $metadata)
	{
		if ($this->persistedId !== null) {
			return $this->persistedId;
		} elseif (!$metadata->isVirtual) {
			return $value;
		}

		$id = [];
		$keys = $this->getMetadata()->getPrimaryKey();
		foreach ($keys as $key) {
			$id[] = $value = $this->getRawValue($key);
		}
		if (count($keys) === 1) {
			return $id[0];
		} else {
			return $id;
		}
	}

	private function internalSetValue(PropertyMetadata $metadata, $name, $value)
	{
		if (!isset($this->validated[$name])) {
			$this->initProperty($metadata, $name);
		}

		if ($this->data[$name] instanceof IPropertyContainer) {
			$this->data[$name]->setInjectedValue($value);
			return;
		}

		if ($metadata->hasSetter) {
			$value = call_user_func([$this, $metadata->hasSetter], $value, $metadata);
			if ($value === IEntity::SKIP_SET_VALUE) {
				$this->modified[$name] = true;
				return;
			}
		}

		$this->validate($metadata, $name, $value);
		$this->data[$name] = $value;
		$this->modified[$name] = true;
	}

	private function &internalGetValue(PropertyMetadata $metadata, $name)
	{
		if (!isset($this->validated[$name])) {
			$this->initProperty($metadata, $name);
		}

		if ($this->data[$name] instanceof IPropertyContainer) {
			return $this->data[$name]->getInjectedValue();
		}

		if ($metadata->hasGetter) {
			$value = call_user_func(
				[$this, $metadata->hasGetter],
				$metadata->isVirtual ? null : $this->data[$name],
				$metadata
			);
		} else {
			$value = $this->data[$name];
		}
		if (!isset($value) && !$metadata->isNullable) {
			$class = get_class($this);
			throw new InvalidStateException("Property {$class}::\${$name} is not set.");
		}
		return $value;
	}

	private function internalHasValue(PropertyMetadata $metadata, $name)
	{
		if (!isset($this->validated[$name])) {
			$this->initProperty($metadata, $name);
		}

		if ($this->data[$name] instanceof IPropertyContainer) {
			return $this->data[$name]->hasInjectedValue();

		} elseif ($metadata->hasGetter) {
			$value = call_user_func(
				[$this, $metadata->hasGetter],
				$metadata->isVirtual ? null : $this->data[$name],
				$metadata
			);
			return isset($value);

		} else {
			return isset($this->data[$name]);
		}
	}

	/**
	 * Validates the value.
	 * @param  PropertyMetadata $metadata
	 * @param  string $name
	 * @param  mixed $value
	 * @throws InvalidArgumentException
	 */
	protected function validate(PropertyMetadata $metadata, $name, & $value)
	{
		if (!$metadata->isValid($value)) {
			$class = get_class($this);
			throw new InvalidArgumentException("Value for {$class}::\${$name} property is invalid.");
		}
	}

	/**
	 * @param PropertyMetadata $metadata
	 * @return IProperty $property
	 */
	protected function createPropertyContainer(PropertyMetadata $metadata)
	{
		$class = $metadata->container;
		return new $class($this, $metadata);
	}

	private function initProperty(PropertyMetadata $metadata, $name)
	{
		$this->validated[$name] = true;

		if (!isset($this->data[$name]) && !array_key_exists($name, $this->data)) {
			$this->data[$name] = $this->persistedId === null ? $metadata->defaultValue : null;
		}

		if ($metadata->container) {
			$property = $this->createPropertyContainer($metadata);
			$property->setRawValue($this->data[$name]);
			$this->data[$name] = $property;

		} elseif ($this->data[$name] !== null) {
			$this->internalSetValue($metadata, $name, $this->data[$name]);
			unset($this->modified[$name]);
		}
	}

	private function attach(IRepository $repository)
	{
		if ($this->repository !== null && $this->repository !== $repository) {
			throw new InvalidStateException('Entity is already attached.');
		}

		$this->repository = $repository;
	}
}


code:
<?php

/**
 * This file is part of the Nextras\Orm library.
 * @license    MIT
 * @link       https://github.com/nextras/orm
 */

namespace Nextras\Orm\Entity;

use Nextras\Orm\NotSupportedException;

class Entity extends AbstractEntity implements IEntity
{
	public function &__get($name)
	{
		$var = $this->getValue($name);
		return $var;
	}

	public function __set($name, $value)
	{
		$this->setValue($name, $value);
	}

	public function __isset($name)
	{
		return $this->hasValue($name);
	}

	public function __unset($name)
	{
		throw new NotSupportedException;
	}

	public function serialize()
	{
		return serialize(parent::serialize());
	}

	public function unserialize($serialized)
	{
		parent::unserialize(unserialize($serialized));
	}
}

repo_name: Cerx/ToDo

license: mit


code:
<?php

/**
 * This file is part of the Nextras\Orm library.
 * @license    MIT
 * @link       https://github.com/nextras/orm
 */

namespace Nextras\Orm\Entity;

use Nextras\Orm\Collection\IEntityPreloadContainer;
use Nextras\Orm\Entity\Reflection\EntityMetadata;
use Nextras\Orm\Model\IModel;
use Nextras\Orm\Repository\IRepository;
use Serializable;

interface IEntity extends Serializable
{
	/**
	 * @const
	 * IRelationshipContainer property is returned as IEntity entity.
	 * IRelationshipCollection property is returned as array of its IEntity entities.
	 * Other properties are not changed.
	 */
	const TO_ARRAY_RELATIONSHIP_AS_IS = 1;

	/**
	 * @const
	 * IRelationshipContainer property is returned as entity id.
	 * IRelationshipCollection property is returned as array of entity ids.
	 * Other properties are not changed.
	 */
	const TO_ARRAY_RELATIONSHIP_AS_ID = 2;

	/**
	 * @const
	 * IRelationshipContainer property is returned as array (entity tranformed to array).
	 * IRelationshipCollection property is returned as array of array (entities tranformed to array).
	 * Other properties are not changed.
	 */
	const TO_ARRAY_RELATIONSHIP_AS_ARRAY = 3;

	/**
	 * @const
	 * Skips setting return value form setter.
	 */
	const SKIP_SET_VALUE = "\0";

	/**
	 * Returns entity model.
	 * @param  bool $need
	 * @return IModel
	 */
	public function getModel($need = true);

	/**
	 * Returns entity repository.
	 * @param  bool $need
	 * @return IRepository|null
	 */
	public function getRepository($need = true);

	/**
	 * Fires event.
	 * @param  string   $method
	 * @param  array    $args
	 */
	public function fireEvent($method, $args = []);

	/**
	 * Sets property value.
	 * @param  string   $name
	 * @param  mixed    $value
	 * @return self
	 */
	public function setValue($name, $value);

	/**
	 * Sets read-only value.
	 * @param  string   $name
	 * @param  mixed    $value
	 * @return self
	 */
	public function setReadOnlyValue($name, $value);

	/**
	 * Returns value.
	 * @param  string   $name
	 * @return mixed
	 */
	public function &getValue($name);

	/**
	 * Returns true if property has a value (not null).
	 * @param  string   $name
	 * @return bool
	 */
	public function hasValue($name);

	/**
	 * Sets raw value.
	 * @param  string   $name
	 * @param  mixed    $value
	 */
	public function setRawValue($name, $value);

	/**
	 * Returns raw value.
	 * Raw value is normalized value which is suitable unique identification and storing.
	 * @param  string   $name
	 * @return mixed
	 */
	public function &getRawValue($name);

	/**
	 * Returns property contents.
	 * @param  string   $name
	 * @return mixed|IPropertyContainer
	 */
	public function getProperty($name);

	/**
	 * Returns property raw contents.
	 * @param  string  $name
	 * @return mixed
	 */
	public function getRawProperty($name);

	/**
	 * Converts entity to array.
	 * @param  int  $mode
	 * @return array
	 */
	public function toArray($mode = self::TO_ARRAY_RELATIONSHIP_AS_IS);

	/**
	 * Returns entity metadata.
	 * @return EntityMetadata
	 */
	public function getMetadata();

	/**
	 * Returns true if the entity is modiefied or the column $name is modified.
	 * @param  string   $name
	 * @return bool
	 */
	public function isModified($name = null);

	/**
	 * Sets the entity or the column as modified.
	 * @param  string   $name
	 * @return self
	 */
	public function setAsModified($name = null);

	/**
	 * Returns true if entity is persisted.
	 * @return bool
	 */
	public function isPersisted();

	/**
	 * Returns persisted primary value.
	 * @return mixed
	 */
	public function getPersistedId();

	/**
	 * Returns true if entity is attached to its repository.
	 * @return bool
	 */
	public function isAttached();

	/**
	 * Sets the collection of entites for the loading relations at once.
	 * @param  IEntityPreloadContainer|null     $overIterator
	 */
	public function setPreloadContainer(IEntityPreloadContainer $overIterator = null);

	/**
	 * @return IEntityPreloadContainer
	 */
	public function getPreloadContainer();
}


code:
<?php

/**
 * This file is part of the Nextras\Orm library.
 * @license    MIT
 * @link       https://github.com/nextras/orm
 */

namespace Nextras\Orm\Entity;

use Nextras\Orm\Entity\Reflection\PropertyMetadata;

interface IProperty
{
	public function __construct(IEntity $entity, PropertyMetadata $propertyMetadata);

	/**
	 * Sets raw value.
	 * @param  mixed $value
	 */
	public function setRawValue($value);

	/**
	 * Returns raw value.
	 * Raw value is normalized value which is suitable unique identification and storing.
	 * @return mixed
	 */
	public function getRawValue();
}


code:
<?php

/**
 * This file is part of the Nextras\Orm library.
 * @license    MIT
 * @link       https://github.com/nextras/orm
 */

namespace Nextras\Orm\Entity;

interface IPropertyContainer extends IProperty
{
	/**
	 * Sets value.
	 * @internal
	 * @param mixed $value
	 */
	public function setInjectedValue($value);

	/**
	 * Returns injected value.
	 * @internal
	 * @return mixed
	 */
	public function &getInjectedValue();

	/**
	 * Returns wheter property container has a value.
	 * @return bool
	 */
	public function hasInjectedValue();
}


code:
<?php

/**
 * This file is part of the Nextras\Orm library.
 * @license    MIT
 * @link       https://github.com/nextras/orm
 */

namespace Nextras\Orm\Entity\Reflection;

use Nette\Object;
use Nextras\Orm\InvalidArgumentException;

/**
 * @property-read string $className
 */
class EntityMetadata extends Object
{
	/** @var string */
	private $className;

	/** @var array Primary key. */
	private $primaryKey = [];

	/** @var PropertyMetadata[] */
	private $properties = [];

	public function __construct($className)
	{
		$this->className = $className;
	}

	public function getClassName()
	{
		return $this->className;
	}

	public function setPrimaryKey(array $primaryKey)
	{
		$this->primaryKey = $primaryKey;
		return $this;
	}

	/**
	 * @return array
	 */
	public function getPrimaryKey()
	{
		return $this->primaryKey;
	}

	/**
	 * @param  string   $name
	 * @return PropertyMetadata
	 */
	public function getProperty($name)
	{
		if (!isset($this->properties[$name])) {
			throw new InvalidArgumentException("Undefined property {$this->className}::\${$name}.");
		}

		return $this->properties[$name];
	}

	/**
	 * @param  string   $name
	 * @return bool
	 */
	public function hasProperty($name)
	{
		return isset($this->properties[$name]);
	}

	/**
	 * @param string            $name
	 * @param PropertyMetadata  $property
	 */
	public function setProperty($name, PropertyMetadata $property)
	{
		$this->properties[$name] = $property;
	}

	/**
	 * @return PropertyMetadata[]
	 */
	public function getProperties()
	{
		return $this->properties;
	}
}


code:
<?php

/**
 * This file is part of the Nextras\Orm library.
 * @license    MIT
 * @link       https://github.com/nextras/orm
 */

namespace Nextras\Orm\Entity\Reflection;

interface IMetadataParser
{
	/**
	 * Parses metadata for entity.
	 * @param  string $entityClass
	 * @param  array|null $fileDependencies
	 * @return EntityMetadata
	 */
	public function parseMetadata($entityClass, & $fileDependencies);
}


code:
<?php

/**
 * This file is part of the Nextras\Orm library.
 * @license    MIT
 * @link       https://github.com/nextras/orm
 */

namespace Nextras\Orm\Entity\Reflection;

interface IMetadataParserFactory
{
	/**
	 * Creates metadata parser.
	 * @param  array $entityClassesMap
	 * @return \Nextras\Orm\Entity\Reflection\IMetadataParser
	 */
	public function create(array $entityClassesMap);
}


code:
<?php

/**
 * This file is part of the Nextras\Orm library.
 * @license    MIT
 * @link       https://github.com/nextras/orm
 */

namespace Nextras\Orm\Entity\Reflection;

use Nette\Reflection\AnnotationsParser;
use Nette\Reflection\ClassType;
use Nextras\Orm\Collection\ICollection;
use Nextras\Orm\Entity\IProperty;
use Nextras\Orm\InvalidArgumentException;
use Nextras\Orm\InvalidModifierDefinitionException;
use Nextras\Orm\InvalidStateException;
use Nextras\Orm\Relationships\ManyHasMany;
use Nextras\Orm\Relationships\ManyHasOne;
use Nextras\Orm\Relationships\OneHasMany;
use Nextras\Orm\Relationships\OneHasOne;

class MetadataParser implements IMetadataParser
{
	/** @var array */
	protected $modifiers = [
		'1:1' => 'parseOneHasOne',
		'1:1d' => 'parseOneHasOne',
		'1:m' => 'parseOneHasMany',
		'1:n' => 'parseOneHasMany',
		'm:1' => 'parseManyHasOne',
		'n:1' => 'parseManyHasOne',
		'm:m' => 'parseManyHasMany',
		'm:n' => 'parseManyHasMany',
		'n:m' => 'parseManyHasMany',
		'n:n' => 'parseManyHasMany',
		'enum' => 'parseEnum',
		'virtual' => 'parseVirtual',
		'container' => 'parseContainer',
		'default' => 'parseDefault',
		'primary' => 'parsePrimary',
		'primary-proxy' => 'parsePrimaryProxy',
	];

	/** @var ClassType */
	protected $reflection;

	/** @var ClassType */
	protected $currentReflection;

	/** @var EntityMetadata */
	protected $metadata;

	/** @var array */
	protected $entityClassesMap;

	/** @var ModifierParser */
	protected $modifierParser;

	public function __construct(array $entityClassesMap)
	{
		$this->entityClassesMap = $entityClassesMap;
		$this->modifierParser = new ModifierParser();
	}

	/**
	 * Adds modifier processor.
	 * @param  string $modifier
	 * @param  callable $processor
	 * @return self
	 */
	public function addModifier($modifier, callable $processor)
	{
		$this->modifiers[strtolower($modifier)] = $processor;
		return $this;
	}

	public function parseMetadata($class, & $fileDependencies)
	{
		$this->reflection = new ClassType($class);
		$this->metadata = new EntityMetadata($class);

		$this->loadProperties($fileDependencies);
		$this->loadGettersSetters();
		$this->initPrimaryKey();

		$fileDependencies = array_unique($fileDependencies);
		return $this->metadata;
	}

	protected function loadGettersSetters()
	{
		$methods = [];
		foreach ($this->reflection->getMethods() as $method) {
			$methods[strtolower($method->name)] = $method;
		}

		foreach ($this->metadata->getProperties() as $name => $property) {
			$getter = 'getter' . strtolower($name);
			if (isset($methods[$getter])) {
				$property->hasGetter = $getter;
			}
			$setter = 'setter' . strtolower($name);
			if (isset($methods[$setter])) {
				$property->hasSetter = $setter;
			}
		}
	}

	protected function loadProperties(& $fileDependencies)
	{
		$classTree = [$current = $this->reflection->name];
		while (($current = get_parent_class($current)) !== false) {
			$classTree[] = $current;
		}

		foreach (array_reverse($classTree) as $class) {
			$reflection = ClassType::from($class);
			$fileDependencies[] = $reflection->getFileName();
			$this->currentReflection = $reflection;
			$this->parseAnnotations($reflection);
		}
	}

	protected function parseAnnotations(ClassType $reflection)
	{
		foreach ($reflection->getAnnotations() as $annotation => $values) {
			if ($annotation === 'property') {
				$isReadonly = false;
			} elseif ($annotation === 'property-read') {
				$isReadonly = true;
			} else {
				continue;
			}

			foreach ($values as $value) {
				$splitted = preg_split('#\s+#', $value, 3);
				if (count($splitted) < 2 || $splitted[1][0] !== '$') {
					throw new InvalidArgumentException("Annotation syntax error '$value'.");
				}

				$property = new PropertyMetadata();
				$property->name = substr($splitted[1], 1);
				$property->isReadonly = $isReadonly;
				$this->metadata->setProperty($property->name, $property);

				$this->parseAnnotationTypes($property, $splitted[0]);
				$this->parseAnnotationValue($property, isset($splitted[2]) ? $splitted[2] : null);
			}
		}
	}

	protected function parseAnnotationTypes(PropertyMetadata $property, $typesString)
	{
		static $allTypes = [
			'array', 'bool', 'boolean', 'double', 'float', 'int', 'integer', 'mixed',
			'numeric', 'number', 'null', 'object', 'real', 'string', 'text', 'void',
			'datetime', 'datetimeimmutable', 'scalar',
		];
		static $alliases = [
			'double' => 'float',
			'real' => 'float',
			'numeric' => 'float',
			'number' => 'float',
			'integer' => 'int',
			'boolean' => 'bool',
		];

		$types = [];
		foreach (explode('|', $typesString) as $type) {
			if (strpos($type, '[') !== false) { // Class[]
				$type = 'array';
			} elseif (!in_array(strtolower($type), $allTypes, true)) {
				$type = $this->makeFQN($type);
			} elseif (isset($alliases[strtolower($type)])) {
				$type = $alliases[strtolower($type)];
			}
			$types[$type] = true;
		}

		$property->isNullable = isset($types['null']) || isset($types['NULL']);
		unset($types['null'], $types['NULL']);
		$property->types = $types;
	}

	protected function parseAnnotationValue(PropertyMetadata $property, $propertyComment)
	{
		if (!$propertyComment) {
			return;
		}

		$matches = $this->modifierParser->matchModifiers($propertyComment);
		foreach ($matches as $macroContent) {
			try {
				$args = $this->modifierParser->parse($macroContent, $this->currentReflection);
			} catch (InvalidModifierDefinitionException $e) {
				throw new InvalidModifierDefinitionException(
					"Invalid modifier definition for {$this->currentReflection->name}::\${$property->name} property.",
					0,
					$e
				);
			}
			$this->processPropertyModifier($property, $args[0], $args[1]);
		}
	}

	protected function processPropertyModifier(PropertyMetadata $property, $modifier, array $args)
	{
		$type = strtolower($modifier);
		if (!isset($this->modifiers[$type])) {
			throw new InvalidModifierDefinitionException(
				"Unknown modifier '$type' type for {$this->currentReflection->name}::\${$property->name} property."
			);
		}

		$callback = $this->modifiers[$type];
		if (!is_array($callback)) {
			$callback = [$this, $callback];
		}
		call_user_func_array($callback, [$property, &$args]);
		if (!empty($args)) {
			$parts = [];
			foreach ($args as $key => $val) {
				if (is_numeric($key) && !is_array($val)) {
					$parts[] = $val;
					continue;
				}
				$parts[] = $key;
			}
			throw new InvalidModifierDefinitionException(
				"Modifier {{$type}} in {$this->currentReflection->name}::\${$property->name} property has unknown arguments: " . implode(', ', $parts) . '.'
			);
		}
	}

	protected function parseOneHasOne(PropertyMetadata $property, array &$args)
	{
		$property->relationship = new PropertyRelationshipMetadata();
		$property->relationship->type = PropertyRelationshipMetadata::ONE_HAS_ONE;
		$property->container = OneHasOne::class;
		$this->processRelationshipIsMain($args, $property);
		$this->processRelationshipEntityProperty($args, $property);
		$this->processRelationshipCascade($args, $property);
	}

	protected function parseOneHasMany(PropertyMetadata $property, array &$args)
	{
		$property->relationship = new PropertyRelationshipMetadata();
		$property->relationship->type = PropertyRelationshipMetadata::ONE_HAS_MANY;
		$property->container = OneHasMany::class;
		$this->processRelationshipEntityProperty($args, $property);
		$this->processRelationshipCascade($args, $property);
		$this->processRelationshipOrder($args, $property);
	}

	protected function parseManyHasOne(PropertyMetadata $property, array &$args)
	{
		$property->relationship = new PropertyRelationshipMetadata();
		$property->relationship->type = PropertyRelationshipMetadata::MANY_HAS_ONE;
		$property->container = ManyHasOne::class;
		$this->processRelationshipEntityProperty($args, $property);
		$this->processRelationshipCascade($args, $property);
	}

	protected function parseManyHasMany(PropertyMetadata $property, array &$args)
	{
		$property->relationship = new PropertyRelationshipMetadata();
		$property->relationship->type = PropertyRelationshipMetadata::MANY_HAS_MANY;
		$property->container = ManyHasMany::class;
		$this->processRelationshipIsMain($args, $property);
		$this->processRelationshipEntityProperty($args, $property);
		$this->processRelationshipCascade($args, $property);
		$this->processRelationshipOrder($args, $property);
	}

	private function processRelationshipEntityProperty(array &$args, PropertyMetadata $property)
	{
		static $modifiersMap = [
			PropertyRelationshipMetadata::ONE_HAS_MANY => '1:m',
			PropertyRelationshipMetadata::ONE_HAS_ONE => '1:1',
			PropertyRelationshipMetadata::MANY_HAS_ONE => 'm:1',
			PropertyRelationshipMetadata::MANY_HAS_MANY => 'm:m',
		];
		$modifier = $modifiersMap[$property->relationship->type];
		$class = array_shift($args);

		if ($class === null) {
			throw new InvalidModifierDefinitionException("Relationship {{$modifier}} in {$this->currentReflection->name}::\${$property->name} has not defined target entity and its property name.");
		}

		if (($pos = strpos($class, '::')) === false) {
			if (preg_match('#^[a-z0-9_\\\\]+$#i', $class) === 0) {
				throw new InvalidModifierDefinitionException("Relationship {{$modifier}} in {$this->currentReflection->name}::\${$property->name} has invalid class name of the target entity. Use Entity::\$property format.");
			} elseif (!(isset($args['oneSided']) && $args['oneSided'])) {
				throw new InvalidModifierDefinitionException("Relationship {{$modifier}} in {$this->currentReflection->name}::\${$property->name} has not defined target property name.");
			} else {
				$targetProperty = null;
				unset($args['oneSided']);
			}
		} else {
			$targetProperty = substr($class, $pos + 3); // skip ::$
			$class = substr($class, 0, $pos);
		}

		$entity = $this->makeFQN($class);
		if (!isset($this->entityClassesMap[$entity])) {
			throw new InvalidModifierDefinitionException("Relationship {{$modifier}} in {$this->currentReflection->name}::\${$property->name} points to unknown '{$entity}' entity.");
		}

		$property->relationship->entity = $entity;
		$property->relationship->repository = $this->entityClassesMap[$entity];
		$property->relationship->property = $targetProperty;
	}

	private function processRelationshipCascade(array &$args, PropertyMetadata $property)
	{
		$property->relationship->cascade = $defaults = [
			'persist' => false,
			'remove' => false,
		];

		if (!isset($args['cascade'])) {
			$property->relationship->cascade['persist'] = true;
			return;
		}

		foreach ((array) $args['cascade'] as $cascade) {
			if (!isset($defaults[$cascade])) {
				throw new InvalidModifierDefinitionException();
			}
			$property->relationship->cascade[$cascade] = true;
		}
		unset($args['cascade']);
	}

	private function processRelationshipOrder(array &$args, PropertyMetadata $property)
	{
		if (!isset($args['orderBy'])) {
			return;
		}

		$order = (array) $args['orderBy'];
		if (!isset($order[1])) {
			$order[1] = ICollection::ASC;
		}

		$property->relationship->order = $order;
		unset($args['orderBy']);
	}

	private function processRelationshipIsMain(array &$args, PropertyMetadata $property)
	{
		$property->relationship->isMain = (isset($args['primary']) && $args['primary']) || (isset($args['isMain']) && $args['isMain']);
		if (isset($args['primary'])) {
			trigger_error("Primary parameter of relationship modifier in {$this->currentReflection->name}::\${$property->name} property is deprecated. Use isMain parameter.", E_USER_DEPRECATED);
		}
		unset($args['primary'], $args['isMain']);
	}

	protected function parseEnum(PropertyMetadata $property, array &$args)
	{
		$property->enum = $args;
		$args = [];
	}

	protected function parseVirtual(PropertyMetadata $property)
	{
		$property->isVirtual = true;
	}

	protected function parseContainer(PropertyMetadata $property, array &$args)
	{
		$className = $this->makeFQN(array_shift($args));
		if (!class_exists($className)) {
			throw new InvalidModifierDefinitionException("Class '$className' in {container} for {$this->currentReflection->name}::\${$property->name} property does not exist.");
		}
		$implements = class_implements($className);
		if (!isset($implements[IProperty::class])) {
			throw new InvalidModifierDefinitionException("Class '$className' in {container} for {$this->currentReflection->name}::\${$property->name} property does not implement Nextras\\Orm\\Entity\\IProperty interface.");
		}
		$property->container = $className;
	}

	protected function parseDefault(PropertyMetadata $property, array &$args)
	{
		$property->defaultValue = array_shift($args);
	}

	protected function parsePrimary(PropertyMetadata $property)
	{
		$property->isPrimary = true;
	}

	protected function parsePrimaryProxy(PropertyMetadata $property)
	{
		$property->isVirtual = true;
		$property->isPrimary = true;
		if (!$property->hasGetter && !$property->hasSetter) {
			$property->hasGetter = 'getterPrimaryProxy';
			$property->hasSetter = 'setterPrimaryProxy';
		}
	}

	protected function initPrimaryKey()
	{
		$primaryKey = array_values(array_filter(array_map(function (PropertyMetadata $metadata) {
			return $metadata->isPrimary && !$metadata->isVirtual
				? $metadata->name
				: null;
		}, $this->metadata->getProperties())));

		if (empty($primaryKey)) {
			throw new InvalidStateException("Entity {$this->reflection->name} does not have defined any primary key.");
		} elseif (!$this->metadata->hasProperty('id') || !$this->metadata->getProperty('id')->isPrimary) {
			throw new InvalidStateException("Entity {$this->reflection->name} has to have defined \$id property as {primary} or {primary-proxy}.");
		}

		$this->metadata->setPrimaryKey($primaryKey);
	}

	protected function makeFQN($name)
	{
		return AnnotationsParser::expandClassName($name, $this->currentReflection);
	}
}


code:
<?php

/**
 * This file is part of the Nextras\Orm library.
 * @license    MIT
 * @link       https://github.com/nextras/orm
 */

namespace Nextras\Orm\Entity\Reflection;

class MetadataParserFactory implements IMetadataParserFactory
{
	public function create(array $entityClassesMap)
	{
		return new MetadataParser($entityClassesMap);
	}
}


code:
<?php

/**
 * This file is part of the Nextras\Orm library.
 * @license    MIT
 * @link       https://github.com/nextras/orm
 */

namespace Nextras\Orm\Entity\Reflection;

use Nette\Object;
use Nextras\Orm\InvalidStateException;
use Nextras\Orm\Model\IRepositoryLoader;

class MetadataValidator extends Object
{
	/**
	 * @param EntityMetadata[]  $metadata
	 * @param IRepositoryLoader $repositoryLoader
	 */
	public function validate(array $metadata, IRepositoryLoader $repositoryLoader)
	{
		$pairs = [
			PropertyRelationshipMetadata::MANY_HAS_MANY => PropertyRelationshipMetadata::MANY_HAS_MANY,
			PropertyRelationshipMetadata::MANY_HAS_ONE => PropertyRelationshipMetadata::ONE_HAS_MANY,
			PropertyRelationshipMetadata::ONE_HAS_MANY => PropertyRelationshipMetadata::MANY_HAS_ONE,
			PropertyRelationshipMetadata::ONE_HAS_ONE => PropertyRelationshipMetadata::ONE_HAS_ONE,
		];

		foreach ($metadata as $entityMeta) {
			foreach ($entityMeta->getProperties() as $propertyMeta) {
				if (!$propertyMeta->relationship) {
					continue;
				}

				$repositoryName = $propertyMeta->relationship->repository;
				if (!$repositoryLoader->hasRepository($repositoryName)) {
					throw new InvalidStateException("{$entityMeta->className}::\${$propertyMeta->name} points to unknown '{$propertyMeta->relationship->repository}' repository.");
				}

				if ($propertyMeta->relationship->property === null) {
					$relType = $propertyMeta->relationship->type;
					$isAllowedOneSided
						= ($relType === PropertyRelationshipMetadata::ONE_HAS_ONE && $propertyMeta->relationship->isMain)
						|| ($relType === PropertyRelationshipMetadata::MANY_HAS_ONE)
						|| ($relType === PropertyRelationshipMetadata::MANY_HAS_MANY && $propertyMeta->relationship->isMain);
					if (!$isAllowedOneSided) {
						throw new InvalidStateException("{$entityMeta->className}::\${$propertyMeta->name} must have defined a symetric relationship.");
					} else {
						continue;
					}
				}

				$symetricEntityMeta = $metadata[$propertyMeta->relationship->entity];

				if (!$symetricEntityMeta->hasProperty($propertyMeta->relationship->property)) {
					throw new InvalidStateException("{$entityMeta->className}::\${$propertyMeta->name} has not defined a symetric relationship in {$symetricEntityMeta->className}::\${$propertyMeta->relationship->property}.");
				}

				/** @var PropertyMetadata $symetricPropertyMeta */
				$symetricPropertyMeta = $symetricEntityMeta->getProperty($propertyMeta->relationship->property);
				if ($symetricPropertyMeta->relationship === null) {
					throw new InvalidStateException("{$entityMeta->className}::\${$propertyMeta->name} has not defined a symetric relationship in {$symetricEntityMeta->className}::\${$propertyMeta->relationship->property}.");
				}

				if ($propertyMeta->name !== $symetricPropertyMeta->relationship->property) {
					throw new InvalidStateException("{$entityMeta->className}::\${$propertyMeta->name} relationship with {$symetricEntityMeta->className}::\${$propertyMeta->relationship->property} is not symetric.");
				}

				if ($symetricPropertyMeta->relationship->type !== $pairs[$propertyMeta->relationship->type]) {
					throw new InvalidStateException("{$entityMeta->className}::\${$propertyMeta->name} has not defined a proper reverse relationship type in {$symetricEntityMeta->className}::\${$propertyMeta->relationship->property}.");
				}

				if ($propertyMeta->relationship->type === PropertyRelationshipMetadata::MANY_HAS_MANY || $propertyMeta->relationship->type === PropertyRelationshipMetadata::ONE_HAS_ONE) {
					if ($propertyMeta->relationship->isMain && $symetricPropertyMeta->relationship->isMain) {
						throw new InvalidStateException("Only one side of relationship {$entityMeta->className}::\${$propertyMeta->name} × {$symetricEntityMeta->className}::\${$propertyMeta->relationship->property} could be defined as a primary.");
					} elseif (!$propertyMeta->relationship->isMain && !$symetricPropertyMeta->relationship->isMain) {
						throw new InvalidStateException("At least one side of relationship {$entityMeta->className}::\${$propertyMeta->name} × {$symetricEntityMeta->className}::\${$propertyMeta->relationship->property} has to be defined as a primary.");
					}
				}
			}
		}
	}
}


code:
<?php

/**
 * This file is part of the Nextras\Orm library.
 * @license    MIT
 * @link       https://github.com/nextras/orm
 */

namespace Nextras\Orm\Entity\Reflection;

use Nette\Reflection\AnnotationsParser;
use Nette\Utils\TokenIterator;
use Nette\Utils\Tokenizer;
use Nextras\Orm\InvalidModifierDefinitionException;
use ReflectionClass;

class ModifierParser
{
	/** @internal */
	const TOKEN_KEYWORD = 1;
	/** @internal */
	const TOKEN_STRING = 2;
	/** @internal */
	const TOKEN_LBRACKET = 3;
	/** @internal */
	const TOKEN_RBRACKET = 4;
	/** @internal */
	const TOKEN_EQUAL = 5;
	/** @internal */
	const TOKEN_SEPARATOR = 6;
	/** @internal */
	const TOKEN_WHITESPACE = 7;
	/** @internal regular expression for single & double quoted PHP string */
	const RE_STRING = '\'(?:\\\\.|[^\'\\\\])*\'|"(?:\\\\.|[^"\\\\])*"';

	/** @var Tokenizer */
	private $tokenizer;

	public function __construct()
	{
		$this->tokenizer = new Tokenizer([
			self::TOKEN_STRING => self::RE_STRING,
			self::TOKEN_LBRACKET => '\[',
			self::TOKEN_RBRACKET => '\]',
			self::TOKEN_EQUAL => '=',
			self::TOKEN_KEYWORD => '[a-zA-Z0-9_:$.*>\\\\-]+',
			self::TOKEN_SEPARATOR => ',',
			self::TOKEN_WHITESPACE => '\s*',
		]);
	}

	/**
	 * @param  string $input
	 * @return array
	 */
	public function matchModifiers($input)
	{
		preg_match_all('#
			\{(
				(?:
					' . self::RE_STRING . ' |
					[^}]
				)++
			)\}#x', $input, $matches);
		return $matches[1];
	}

	/**
	 * @param  string $string
	 * @param  ReflectionClass $reflectionClass
	 * @return array
	 */
	public function parse($string, ReflectionClass $reflectionClass)
	{
		$tokens = $this->lex($string, $reflectionClass);
		$iterator = new TokenIterator($tokens);
		return [
			$name = $this->processName($iterator),
			$this->processArgs($iterator, $name, false),
		];
	}

	private function lex($input, ReflectionClass $reflectionClass)
	{
		try {
			$tokens = $this->tokenizer->tokenize($input);
		} catch (TokenizerException $e) {
			throw new InvalidModifierDefinitionException('Unable to tokenize the modifier.', 0, $e);
		}

		$tokens = array_filter($tokens, function ($pair) {
			return $pair[2] !== self::TOKEN_WHITESPACE && $pair[2] !== null;
		});
		$tokens = array_values($tokens);

		$expanded = [];
		foreach ($tokens as $token) {
			if ($token[2] === self::TOKEN_STRING) {
				$token[0] = stripslashes(substr($token[0], 1, -1));
				$expanded[] = $token;

			} elseif ($token[2] === self::TOKEN_KEYWORD) {
				$values = $this->processKeyword($token[0], $reflectionClass);
				if (is_array($values)) {
					$count = count($values) - 1;
					foreach ($values as $i => $value) {
						$expanded[] = [$value, $token[1], $token[2]];
						if ($i !== $count) {
							$expanded[] = [',', -1, self::TOKEN_SEPARATOR];
						}
					}
				} else {
					$expanded[] = [$values, $token[1], $token[2]];
				}

			} else {
				$expanded[] = $token;
			}
		}
		return $expanded;
	}

	private function processName(TokenIterator $iterator)
	{
		$iterator->position++;
		if (!isset($iterator->tokens[$iterator->position])) {
			throw new InvalidModifierDefinitionException("Modifier does not have a name.");
		}
		list($value, , $type) = $iterator->currentToken();
		if ($type !== self::TOKEN_KEYWORD) {
			throw new InvalidModifierDefinitionException("Modifier does not have a name.");
		} elseif (isset($iterator->tokens[$iterator->position + 1])) {
			list(, , $type) = $iterator->tokens[$iterator->position + 1];
			if ($type === self::TOKEN_SEPARATOR) {
				throw new InvalidModifierDefinitionException("After the {{$value}}'s modifier name cannot be a comma separator.");
			}
		}

		return $value;
	}

	private function processArgs(TokenIterator $iterator, $modifierName, $inArray)
	{
		$result = [];
		$iterator->position++;
		while (isset($iterator->tokens[$iterator->position])) {
			list($value, , $type) = $iterator->currentToken();

			if ($type === self::TOKEN_RBRACKET) {
				if ($inArray) {
					return $result;
				} else {
					throw new InvalidModifierDefinitionException("Modifier {{$modifierName}} mismatches brackets.");
				}
			} elseif ($type === self::TOKEN_STRING || $type === self::TOKEN_KEYWORD) {
				$iterator->position++;
				list(, , $nextToken) = $iterator->currentToken();

				if ($nextToken === self::TOKEN_EQUAL) {
					$iterator->position++;
					list(, , $nextToken) = $iterator->currentToken();
					$nextValue = $iterator->currentValue();

					if ($nextToken === self::TOKEN_LBRACKET) {
						$result[$value] = $this->processArgs($iterator, $modifierName, true);
					} elseif ($nextToken === self::TOKEN_STRING || $nextToken === self::TOKEN_KEYWORD) {
						$result[$value] = $nextValue;
					} elseif ($nextToken !== null) {
						throw new InvalidModifierDefinitionException("Modifier {{$modifierName}} has invalid token after =.");
					}
				} elseif ($type !== null) {
					$iterator->position--;
					$result[] = $value;
				}
			} else {
				throw new InvalidModifierDefinitionException("Modifier {{$modifierName}} has invalid token, expected string or keyword.");
			}

			$iterator->position++;
			list(, , $type) = $iterator->currentToken();
			if ($type === self::TOKEN_RBRACKET && $inArray) {
				return $result;
			} elseif ($type !== null && $type !== self::TOKEN_SEPARATOR) {
				throw new InvalidModifierDefinitionException("Modifier {{$modifierName}} misses argument separator.");
			}

			$iterator->position++;
		}

		if ($inArray) {
			throw new InvalidModifierDefinitionException("Modifier {{$modifierName}} has unclosed array argument.");
		}

		return $result;
	}

	private function processKeyword($value, ReflectionClass $reflectionClass)
	{
		if (strcasecmp($value, 'true') === 0) {
			return true;
		} elseif (strcasecmp($value, 'false') === 0) {
			return false;
		} elseif (strcasecmp($value, 'null') === 0) {
			return null;
		} elseif (is_numeric($value)) {
			return $value * 1;
		} elseif (preg_match('#^[a-z0-9_\\\\]+::[a-z0-9_]+(\\*)?$#i', $value)) {
			list($className, $const) = explode('::', $value, 2);
			if ($className === 'self' || $className === 'static') {
				$reflection = $reflectionClass;
			} else {
				$className = AnnotationsParser::expandClassName($className, $reflectionClass);
				$reflection = new ReflectionClass($className);
			}

			$enum = [];
			$constants = $reflection->getConstants();
			if (strpos($const, '*') !== false) {
				$prefix = rtrim($const, '*');
				$prefixLength = strlen($prefix);
				$count = 0;
				foreach ($constants as $name => $value) {
					if (substr($name, 0, $prefixLength) === $prefix) {
						$enum[$value] = $value;
						$count += 1;
					}
				}
				if ($count === 0) {
					throw new InvalidModifierDefinitionException("No constant matches {$reflection->name}::{$const} pattern.");
				}
			} else {
				if (!array_key_exists($const, $constants)) {
					throw new InvalidModifierDefinitionException("Constant {$reflection->name}::{$const} does not exist.");
				}
				$value = $reflection->getConstant($const);
				$enum[$value] = $value;
			}
			return array_values($enum);
		} else {
			return $value;
		}
	}
}


code:
<?php

/**
 * This file is part of the Nextras\Orm library.
 * This file was inspired by YetORM https://github.com/uestla/YetORM/.
 * @license    MIT
 * @link       https://github.com/nextras/orm
 */

namespace Nextras\Orm\Entity\Reflection;

use DateTimeZone;
use Nette\Object;
use stdClass;

class PropertyMetadata extends Object
{
	/** @var string property name */
	public $name = '';

	/** @var string|null */
	public $container;

	/** @var string|null */
	public $hasGetter;

	/** @var string|null */
	public $hasSetter;

	/** @var array of allowed types defined as keys */
	public $types = [];

	/** @var bool */
	public $isPrimary = false;

	/** @var bool */
	public $isNullable = false;

	/** @var bool */
	public $isReadonly = false;

	/** @var bool */
	public $isVirtual = false;

	/** @var mixed */
	public $defaultValue;

	/** @var PropertyRelationshipMetadata|null */
	public $relationship;

	/** @var stdClass|null */
	public $args;

	/** @var mixed[]|null array of alowed values */
	public $enum;

	public function isValid(& $value)
	{
		if ($value === null && $this->isNullable) {
			return true;
		}

		if ($this->enum) {
			return in_array($value, $this->enum, true);
		}

		foreach ($this->types as $type => $_) {
			$type = strtolower($type);
			if ($type === 'datetime') {
				if ($value instanceof \DateTime) {
					return true;

				} elseif ($value instanceof \DateTimeImmutable) {
					$value = new \DateTime($value->format('c'));
					return true;

				} elseif (is_string($value) && $value !== '') {
					$value = new \DateTime($value);
					$value->setTimezone(new DateTimeZone(date_default_timezone_get()));
					return true;

				} elseif (ctype_digit($value)) {
					$value = new \DateTime("@{$value}");
					return true;
				}

			} elseif ($type === 'datetimeimmutable') {
				if ($value instanceof \DateTimeImmutable) {
					return true;

				} elseif ($value instanceof \DateTime) {
					$value = new \DateTimeImmutable($value->format('c'));
					return true;

				} elseif (is_string($value) && $value !== '') {
					$tmp = new \DateTimeImmutable($value);
					$value = $tmp->setTimezone(new DateTimeZone(date_default_timezone_get()));
					return true;

				} elseif (ctype_digit($value)) {
					$value = new \DateTimeImmutable("@{$value}");
					return true;
				}

			} elseif ($type === 'string') {
				if (is_string($value)) {
					return true;
				}
				if (is_int($value) || (is_object($value) && method_exists($value, '__toString'))) {
					$value = (string) $value;
					return true;
				}

			} elseif ($type === 'float') {
				if (is_float($value)) {
					return true;
				}
				if (is_numeric($value)) {
					settype($value, 'float');
					return true;
				} elseif (is_string($value)) {
					$value = (float) str_replace([' ', ','], ['', '.'], $value);
					return true;
				}

			} elseif ($type === 'int') {
				if (is_int($value)) {
					return true;
				}
				if (is_numeric($value)) {
					settype($value, 'int');
					return true;
				} elseif (is_string($value)) {
					$value = (int) str_replace([' ', ','], ['', '.'], $value);
					return true;
				}

			} elseif ($type === 'bool') {
				if (is_bool($value)) {
					return true;
				}
				if (in_array($value, [0, 0.0, '0', 1, 1.0, '1'], true)) {
					$value = (bool) $value;
					return true;
				}

			} elseif ($type === 'array') {
				if (is_array($value)) {
					return true;
				}

			} elseif ($type === 'object') {
				if (is_object($value)) {
					return true;
				}

			} elseif ($type === 'scalar') {
				if (is_scalar($value)) {
					return true;
				}

			} elseif ($type === 'mixed') {
				return true;

			} else {
				if ($value instanceof $type) {
					return true;
				}
			}
		}

		return false;
	}
}


code:
<?php

/**
 * This file is part of the Nextras\Orm library.
 * @license    MIT
 * @link       https://github.com/nextras/orm
 */

namespace Nextras\Orm\Entity\Reflection;

class PropertyRelationshipMetadata
{
	const ONE_HAS_ONE = 1;
	const ONE_HAS_MANY = 2;
	const MANY_HAS_ONE = 3;
	const MANY_HAS_MANY = 4;

	/** @var string */
	public $repository;

	/** @var string */
	public $entity;

	/** @var string */
	public $property;

	/** @var bool */
	public $isMain = false;

	/** @var int */
	public $type;

	/** @var array */
	public $order;

	/** @var bool[] */
	public $cascade;
}


code:
<?php

/**
 * This file is part of the Nextras\Orm library.
 * This file was inspired by PetrP's ORM library https://github.com/PetrP/Orm/.
 * @license    MIT
 * @link       https://github.com/nextras/orm
 */

namespace Nextras\Orm\Entity;

use Nextras\Orm\Relationships\IRelationshipCollection;

class ToArrayConverter
{
	/** @var int Maximum recursion level. */
	public static $maxRecursionLevel = 3;

	/**
	 * Converts IEntity to array
	 * @param  IEntity  $entity
	 * @param  int      $type
	 * @param  int      $recursionLevel
	 * @return array|null
	 */
	public static function toArray(IEntity $entity, $type = IEntity::TO_ARRAY_RELATIONSHIP_AS_IS, $recursionLevel = 0)
	{
		if ($recursionLevel >= static::$maxRecursionLevel) {
			return null;
		}

		$return = [];
		$metadata = $entity->getMetadata();

		foreach ($metadata->getProperties() as $name => $metadataProperty) {
			if (!$entity->hasValue($name)) {
				$value = null;
			} else {
				$value = $entity->getValue($name);
			}

			if ($value instanceof IEntity) {
				if ($type === IEntity::TO_ARRAY_RELATIONSHIP_AS_ID) {
					$value = $value->getValue('id');
				} elseif ($type === IEntity::TO_ARRAY_RELATIONSHIP_AS_ARRAY) {
					$value = static::toArray($value, $type, $recursionLevel + 1);
				}

			} elseif ($value instanceof IRelationshipCollection) {
				if ($type === IEntity::TO_ARRAY_RELATIONSHIP_AS_ID) {
					$collection = [];
					foreach ($value as $collectionEntity) {
						$collection[] = $collectionEntity->getValue('id');
					}
					$value = $collection;

				} elseif ($type === IEntity::TO_ARRAY_RELATIONSHIP_AS_ARRAY) {
					$collection = [];
					foreach ($value as $collectionEntity) {
						$collection[] = static::toArray($collectionEntity, $type, $recursionLevel + 1);
					}
					$value = $collection;
				}
			}

			$return[$name] = $value;
		}

		return $return;
	}
}


code:
<?php

/**
 * This file is part of the Nextras\Orm library.
 * @license    MIT
 * @link       https://github.com/nextras/orm
 */

namespace Nextras\Orm\Mapper;

use Nette\Object;
use Nextras\Orm\InvalidStateException;
use Nextras\Orm\Repository\IRepository;
use Nextras\Orm\StorageReflection\IStorageReflection;
use Nextras\Orm\StorageReflection\StringHelper;
use stdClass;

abstract class BaseMapper extends Object implements IMapper
{
	/** @var string */
	protected $tableName;

	/** @var IStorageReflection */
	protected $storageReflection;

	/** @var stdClass */
	protected $collectionCache;

	/** @var IRepository */
	private $repository;

	public function setRepository(IRepository $repository)
	{
		if ($this->repository && $this->repository !== $repository) {
			$name = get_class($this);
			throw new InvalidStateException("Mapper '$name' is already attached to repository.");
		}

		$this->repository = $repository;
		$this->collectionCache = (object) null;
	}

	public function getRepository()
	{
		if (!$this->repository) {
			$name = get_class($this);
			throw new InvalidStateException("Mapper '$name' is not attached to repository.");
		}

		return $this->repository;
	}

	public function getTableName()
	{
		if (!$this->tableName) {
			$tableName = str_replace('Mapper', '', $this->getReflection()->getShortName());
			$this->tableName = StringHelper::underscore($tableName);
		}

		return $this->tableName;
	}

	public function getStorageReflection()
	{
		if ($this->storageReflection === null) {
			$this->storageReflection = $this->createStorageReflection();
		}

		return $this->storageReflection;
	}

	public function getCollectionCache()
	{
		return $this->collectionCache;
	}

	public function clearCollectionCache()
	{
		$this->collectionCache = (object) null;
	}

	public function flush()
	{
		$this->collectionCache = (object) null;
	}

	abstract protected function createStorageReflection();
}


code:
<?php

/**
 * This file is part of the Nextras\Orm library.
 * @license    MIT
 * @link       https://github.com/nextras/orm
 */

namespace Nextras\Orm\Mapper\Dbal;

use Iterator;
use Nextras\Dbal\Connection;
use Nextras\Dbal\QueryBuilder\QueryBuilder;
use Nextras\Orm\Collection\EntityIterator;
use Nextras\Orm\Collection\Helpers\FetchPairsHelper;
use Nextras\Orm\Collection\ICollection;
use Nextras\Orm\Entity\IEntity;
use Nextras\Orm\Mapper\Dbal\StorageReflection\StorageReflection;
use Nextras\Orm\Mapper\IRelationshipMapper;
use Nextras\Orm\MemberAccessException;
use Nextras\Orm\Repository\IRepository;

class DbalCollection implements ICollection
{
	/** @var IRelationshipMapper */
	protected $relationshipMapper;

	/** @var IEntity */
	protected $relationshipParent;

	/** @var Iterator */
	protected $fetchIterator;

	/** @var IRepository */
	protected $repository;

	/** @var Connection */
	protected $connection;

	/** @var QueryBuilder */
	protected $queryBuilder;

	/** @var QueryBuilderHelper */
	protected $parser;

	/** @var array|null */
	protected $result;

	/** @var int */
	protected $resultCount;

	/** @var bool */
	protected $distinct = false;

	public function __construct(IRepository $repository, Connection $connection, QueryBuilder $queryBuilder)
	{
		$this->repository = $repository;
		$this->connection = $connection;
		$this->queryBuilder = $queryBuilder;
	}

	public function getBy(array $where)
	{
		return $this->findBy($where)->limitBy(1)->fetch();
	}

	public function findBy(array $where)
	{
		$collection = clone $this;
		$parser = $collection->getParser();

		foreach ($where as $column => $value) {
			$parser->processWhereExpression($column, $value, $collection->queryBuilder, $collection->distinct);
		}

		return $collection;
	}

	public function orderBy($column, $direction = ICollection::ASC)
	{
		$collection = clone $this;
		$parser = $collection->getParser();

		if (is_array($column)) {
			foreach ($column as $col => $direction) {
				$parser->processOrderByExpression($col, $direction, $collection->queryBuilder);
			}
		} else {
			$parser->processOrderByExpression($column, $direction, $collection->queryBuilder);
		}

		return $collection;
	}

	public function resetOrderBy()
	{
		$collection = clone $this;
		$collection->queryBuilder->orderBy(null);
		return $collection;
	}

	public function limitBy($limit, $offset = null)
	{
		$collection = clone $this;
		$collection->queryBuilder->limitBy($limit, $offset);
		return $collection;
	}

	public function fetch()
	{
		if (!$this->fetchIterator) {
			$this->fetchIterator = $this->getIterator();
		}

		if ($current = $this->fetchIterator->current()) {
			$this->fetchIterator->next();
			return $current;
		}

		return null;
	}

	public function fetchAll()
	{
		return iterator_to_array($this->getIterator());
	}

	public function fetchPairs($key = null, $value = null)
	{
		return FetchPairsHelper::process($this->getIterator(), $key, $value);
	}

	/** @deprecated */
	public function toCollection($resetOrderBy = false)
	{
		return $resetOrderBy ? $this->resetOrderBy() : clone $this;
	}

	public function __call($name, $args)
	{
		$class = get_class($this);
		throw new MemberAccessException("Call to undefined method $class::$name().");
	}

	public function getIterator()
	{
		return $this->getEntityIterator($this->relationshipParent);
	}

	public function getEntityIterator(IEntity $parent = null)
	{
		if ($this->relationshipMapper) {
			return $this->relationshipMapper->getIterator($parent, $this);
		}

		if ($this->result === null) {
			$this->execute();
		}

		return new EntityIterator($this->result);
	}

	public function count()
	{
		return iterator_count($this->getIterator());
	}

	public function countStored()
	{
		return $this->getEntityCount($this->relationshipParent);
	}

	public function getEntityCount(IEntity $parent = null)
	{
		if ($this->relationshipMapper) {
			return $this->relationshipMapper->getIteratorCount($parent, $this);
		}

		return $this->getIteratorCount();
	}

	public function setRelationshipMapping(IRelationshipMapper $mapper = null, IEntity $parent = null)
	{
		$this->relationshipMapper = $mapper;
		$this->relationshipParent = $parent;
		return $this;
	}

	public function getRelationshipMapper()
	{
		return $this->relationshipMapper;
	}

	public function __clone()
	{
		$this->queryBuilder = clone $this->queryBuilder;
		$this->result = null;
		$this->resultCount = null;
		$this->fetchIterator = null;
	}

	/**
	 * @internal
	 * @return QueryBuilder
	 */
	public function getQueryBuilder()
	{
		return $this->queryBuilder;
	}

	protected function getIteratorCount()
	{
		if ($this->resultCount === null) {
			$builder = clone $this->queryBuilder;
			if ($builder->hasLimitOffsetClause()) {
				/** @var StorageReflection $reflection */
				$reflection = $this->repository->getMapper()->getStorageReflection();
				$primary = (array) $reflection->getStoragePrimaryKey();
				foreach ($primary as $column) {
					$builder->addSelect('%table.%column', $builder->getFromAlias(), $column);
				}
				$sql = 'SELECT COUNT(*) FROM (' . $builder->getQuerySql() . ') temp';
				$args = $builder->getQueryParameters();

			} else {
				$builder->select('COUNT(*)');
				$builder->orderBy(null);
				$sql = $builder->getQuerySql();
				$args = $builder->getQueryParameters();
			}

			$this->resultCount = $this->connection->queryArgs($sql, $args)->fetchField();
		}

		return $this->resultCount;
	}

	protected function execute()
	{
		$builder = clone $this->queryBuilder;
		$table = $builder->getFromAlias();

		if (!$this->distinct) {
			$builder->select("[$table.*]");
		} else {
			$builder->select("DISTINCT [$table.*]");
		}

		$result = $this->connection->queryArgs(
			$builder->getQuerySql(),
			$builder->getQueryParameters()
		);

		$this->result = [];
		while ($data = $result->fetch()) {
			$this->result[] = $this->repository->hydrateEntity($data->toArray());
		}
	}

	protected function getParser()
	{
		if ($this->parser === null) {
			$this->parser = new QueryBuilderHelper($this->repository->getModel(), $this->repository->getMapper());
		}

		return $this->parser;
	}
}


code:
<?php

/**
 * This file is part of the Nextras\Orm library.
 * @license    MIT
 * @link       https://github.com/nextras/orm
 */

namespace Nextras\Orm\Mapper\Dbal;

use Nette\Caching\Cache;
use Nextras\Dbal\Connection;
use Nextras\Dbal\Platforms\PostgreSqlPlatform;
use Nextras\Dbal\QueryBuilder\QueryBuilder;
use Nextras\Dbal\Result\Result;
use Nextras\Orm\Collection\ArrayCollection;
use Nextras\Orm\Entity\IEntity;
use Nextras\Orm\Entity\IProperty;
use Nextras\Orm\Entity\Reflection\PropertyMetadata;
use Nextras\Orm\Entity\Reflection\PropertyRelationshipMetadata as Relationship;
use Nextras\Orm\InvalidArgumentException;
use Nextras\Orm\Mapper\BaseMapper;
use Nextras\Orm\Mapper\IMapper;

class DbalMapper extends BaseMapper
{
	/** @var Connection */
	protected $connection;

	/** @var Cache */
	protected $cache;

	/** @var array */
	private $cacheRM = [];

	/** @var array */
	private static $transactions = [];

	public function __construct(Connection $connection, Cache $cache)
	{
		$key = md5(json_encode($connection->getConfig()));
		$this->connection = $connection;
		$this->cache = $cache->derive('mapper.' . $key);
	}

	/** @inheritdoc */
	public function findAll()
	{
		return new DbalCollection($this->getRepository(), $this->connection, $this->builder());
	}

	/**
	 * @return QueryBuilder
	 */
	public function builder()
	{
		$tableName = $this->getTableName();
		$builder = new QueryBuilder($this->connection->getDriver());
		$builder->from("[$tableName]", QueryBuilderHelper::getAlias($tableName));
		return $builder;
	}

	/** @inheritdoc */
	public function toCollection($data)
	{
		if ($data instanceof QueryBuilder) {
			return new DbalCollection($this->getRepository(), $this->connection, $data);

		} elseif (is_array($data)) {
			$result = array_map([$this->getRepository(), 'hydrateEntity'], $data);
			return new ArrayCollection($result, $this->getRepository());

		} elseif ($data instanceof Result) {
			$result = [];
			$repository = $this->getRepository();
			foreach ($data as $row) {
				$result[] = $repository->hydrateEntity($row->toArray());
			}
			return new ArrayCollection($result, $this->getRepository());
		}

		throw new InvalidArgumentException('DbalMapper can convert only array|QueryBuilder|Result to ICollection.');
	}

	public function getManyHasManyParameters(PropertyMetadata $sourceProperty, IMapper $targetMapper)
	{
		return [
			$this->getStorageReflection()->getManyHasManyStorageName($targetMapper),
			$this->getStorageReflection()->getManyHasManyStoragePrimaryKeys($targetMapper),
		];
	}

	// == Relationship mappers =========================================================================================

	public function createCollectionManyHasOne(PropertyMetadata $metadata, IEntity $parent)
	{
		return $this->findAll()->setRelationshipMapping(
			$this->getRelationshipMapper(Relationship::MANY_HAS_ONE, $metadata),
			$parent
		);
	}

	public function createCollectionOneHasOne(PropertyMetadata $metadata, IEntity $parent)
	{
		return $this->findAll()->setRelationshipMapping(
			$metadata->relationship->isMain
				? $this->getRelationshipMapper(Relationship::MANY_HAS_ONE, $metadata)
				: $this->getRelationshipMapper(Relationship::ONE_HAS_ONE, $metadata),
			$parent
		);
	}

	public function createCollectionManyHasMany(IMapper $mapperTwo, PropertyMetadata $metadata, IEntity $parent)
	{
		$targetMapper = $metadata->relationship->isMain ? $mapperTwo : $this;
		return $targetMapper->findAll()->setRelationshipMapping(
			$this->getRelationshipMapper(Relationship::MANY_HAS_MANY, $metadata, $mapperTwo),
			$parent
		);
	}

	public function createCollectionOneHasMany(PropertyMetadata $metadata, IEntity $parent)
	{
		return $this->findAll()->setRelationshipMapping(
			$this->getRelationshipMapper(Relationship::ONE_HAS_MANY, $metadata),
			$parent
		);
	}

	protected function getRelationshipMapper($type, PropertyMetadata $metadata, IMapper $otherMapper = null)
	{
		$key = $type . spl_object_hash($metadata) . $metadata->name;
		if (!isset($this->cacheRM[$key])) {
			$this->cacheRM[$key] = $this->createRelationshipMapper($type, $metadata, $otherMapper);
		}
		return $this->cacheRM[$key];
	}

	protected function createRelationshipMapper($type, PropertyMetadata $metadata, IMapper $otherMapper = null)
	{
		switch ($type) {
			case Relationship::MANY_HAS_ONE:
				return new RelationshipMapperManyHasOne($this->connection, $this, $metadata);
			case Relationship::ONE_HAS_ONE:
				return new RelationshipMapperOneHasOne($this->connection, $this, $metadata);
			case Relationship::MANY_HAS_MANY:
				return new RelationshipMapperManyHasMany($this->connection, $this, $otherMapper, $metadata);
			case Relationship::ONE_HAS_MANY:
				return new RelationshipMapperOneHasMany($this->connection, $this, $metadata);
			default:
				throw new InvalidArgumentException();
		}
	}

	/**
	 * @return StorageReflection\IStorageReflection
	 */
	public function getStorageReflection()
	{
		return parent::getStorageReflection();
	}

	protected function createStorageReflection()
	{
		return new StorageReflection\UnderscoredStorageReflection(
			$this->connection,
			$this->getTableName(),
			$this->getRepository()->getEntityMetadata()->getPrimaryKey(),
			$this->cache
		);
	}

	// == Persistence API ==============================================================================================

	public function persist(IEntity $entity)
	{
		$this->beginTransaction();
		$data = $this->entityToArray($entity);
		$data = $this->getStorageReflection()->convertEntityToStorage($data);

		if (!$entity->isPersisted()) {
			$this->processInsert($entity, $data);
			return $entity->hasValue('id')
				? $entity->getValue('id')
				: $this->connection->getLastInsertedId($this->getStorageReflection()->getPrimarySequenceName());

		} else {
			$primary = [];
			$id = (array) $entity->getPersistedId();
			foreach ($this->getStorageReflection()->getStoragePrimaryKey() as $key) {
				$primary[$key] = array_shift($id);
			}

			$this->processUpdate($entity, $data, $primary);
			return $entity->getPersistedId();
		}
	}

	protected function processInsert(IEntity $entity, $data)
	{
		$args = ['INSERT INTO %table %values', $this->getTableName(), $data];
		if ($this instanceof IPersistAutoupdateMapper) {
			$this->processAutoupdate($entity, $args);
		} else {
			$this->connection->queryArgs($args);
		}
	}

	protected function processUpdate(IEntity $entity, $data, $primary)
	{
		$args = ['UPDATE %table SET %set WHERE %and', $this->getTableName(), $data, $primary];
		if ($this instanceof IPersistAutoupdateMapper) {
			$this->processAutoupdate($entity, $args);
		} else {
			$this->connection->queryArgs($args);
		}
	}

	public function getAutoupdateReselectExpression()
	{
		return ['%column[]', ['*']];
	}

	protected function processAutoupdate(IEntity $entity, array $args)
	{
		$platform = $this->connection->getPlatform();
		if ($platform instanceof PostgreSqlPlatform) {
			$this->processPostgreAutoupdate($entity, $args);
		} else {
			$this->processMySQLAutoupdate($entity, $args);
		}
	}

	protected function processPostgreAutoupdate(IEntity $entity, array $args)
	{
		$args[] = 'RETURNING %ex';
		$args[] = $this->getAutoupdateReselectExpression();
		$row = $this->connection->queryArgs($args)->fetch();
		$data = $this->getStorageReflection()->convertStorageToEntity($row->toArray());
		$entity->fireEvent('onRefresh', [$data]);
	}

	protected function processMySQLAutoupdate(IEntity $entity, array $args)
	{
		$this->connection->queryArgs($args);

		$primary = [];
		$id = (array) ($entity->isPersisted() ? $entity->getPersistedId() : $this->connection->getLastInsertedId());
		foreach ($this->getStorageReflection()->getStoragePrimaryKey() as $key) {
			$primary[$key] = array_shift($id);
		}

		$row = $this->connection->query(
			'SELECT %ex FROM %table WHERE %and',
			$this->getAutoupdateReselectExpression(),
			$this->getTableName(),
			$primary
		)->fetch();
		$data = $this->getStorageReflection()->convertStorageToEntity($row->toArray());
		$entity->fireEvent('onRefresh', [$data]);
	}

	public function remove(IEntity $entity)
	{
		$this->beginTransaction();

		$primary = [];
		$id = (array) $entity->getPersistedId();
		foreach ($this->getStorageReflection()->getStoragePrimaryKey() as $key) {
			$primary[$key] = array_shift($id);
		}

		$this->processRemove($entity, $primary);
	}

	protected function processRemove(IEntity $entity, $primary)
	{
		$this->connection->query('DELETE FROM %table WHERE %and', $this->getTableName(), $primary);
	}

	protected function entityToArray(IEntity $entity)
	{
		$return = [];
		$metadata = $entity->getMetadata();

		foreach ($metadata->getProperties() as $name => $metadataProperty) {
			if ($metadataProperty->isVirtual) {
				continue;
			} elseif ($metadataProperty->isPrimary && ($entity->isPersisted() || !$entity->hasValue($name))) {
				continue;
			}

			if ($metadataProperty->relationship !== null) {
				$rel = $metadataProperty->relationship;
				$canSkip
					= $rel->type === Relationship::ONE_HAS_MANY
					|| $rel->type === Relationship::MANY_HAS_MANY
					|| ($rel->type === Relationship::ONE_HAS_ONE && !$rel->isMain);
				if ($canSkip) {
					continue;
				}
			}

			$property = $entity->getProperty($name);
			if ($property instanceof IProperty) {
				$value = $property->getRawValue();

			} else {
				$value = $entity->getValue($name);
			}

			$return[$name] = $value;
		}

		return $return;
	}

	// == Transactions API =============================================================================================

	public function beginTransaction()
	{
		$hash = spl_object_hash($this->connection);
		if (!isset(self::$transactions[$hash])) {
			$this->connection->beginTransaction();
			self::$transactions[$hash] = true;
		}
	}

	public function flush()
	{
		parent::flush();
		$this->cacheRM = [];
		$hash = spl_object_hash($this->connection);
		if (isset(self::$transactions[$hash])) {
			$this->connection->commitTransaction();
			unset(self::$transactions[$hash]);
		}
	}

	public function rollback()
	{
		$hash = spl_object_hash($this->connection);
		if (isset(self::$transactions[$hash])) {
			$this->connection->rollbackTransaction();
			unset(self::$transactions[$hash]);
		}
	}
}


code:
<?php

/**
 * This file is part of the Nextras\Orm library.
 * @license    MIT
 * @link       https://github.com/nextras/orm
 */

namespace Nextras\Orm\Mapper\Dbal;

interface IPersistAutoupdateMapper
{
	/**
	 * Returns reselecting columns
	 * as expandable expression for Dbal's %ex modifier.
	 * @return array
	 */
	public function getAutoupdateReselectExpression();
}


code:
<?php

/**
 * This file is part of the Nextras\Orm library.
 * @license    MIT
 * @link       https://github.com/nextras/orm
 */

namespace Nextras\Orm\Mapper\Dbal;

use Nette\Object;
use Nextras\Dbal\QueryBuilder\QueryBuilder;
use Nextras\Orm\Collection\Helpers\ConditionParserHelper;
use Nextras\Orm\Collection\ICollection;
use Nextras\Orm\Entity\IEntity;
use Nextras\Orm\Entity\Reflection\PropertyRelationshipMetadata as Relationship;
use Nextras\Orm\InvalidArgumentException;
use Nextras\Orm\LogicException;
use Nextras\Orm\Mapper\Dbal\StorageReflection\IStorageReflection;
use Nextras\Orm\Model\IModel;
use Nextras\Orm\Model\MetadataStorage;
use Traversable;

/**
 * QueryBuilderHelper for Nextras\Dbal.
 */
class QueryBuilderHelper extends Object
{
	/** @var IModel */
	private $model;

	/** @var DbalMapper */
	private $mapper;

	/** @var MetadataStorage */
	private $metadataStorage;

	public function __construct(IModel $model, DbalMapper $mapper)
	{
		$this->model = $model;
		$this->mapper = $mapper;
		$this->metadataStorage = $model->getMetadataStorage();
	}

	/**
	 * Transforms orm condition and adds it to QueryBuilder.
	 * @param  string       $expression
	 * @param  mixed        $value
	 * @param  QueryBuilder $builder
	 * @param  bool         $distinctNeeded
	 */
	public function processWhereExpression($expression, $value, QueryBuilder $builder, & $distinctNeeded)
	{
		list($chain, $operator, $sourceEntity) = ConditionParserHelper::parseCondition($expression);

		if ($value instanceof Traversable) {
			$value = iterator_to_array($value);
		} elseif ($value instanceof IEntity) {
			$value = $value->getValue('id');
		}

		if (is_array($value) && count($value) === 0) {
			$builder->andWhere($operator === ConditionParserHelper::OPERATOR_EQUAL ? '1=0' : '1=1');
			return;
		}

		$sqlExpresssion = $this->normalizeAndAddJoins($chain, $sourceEntity, $builder, $distinctNeeded, $value, $modifier);
		$operator = $this->getSqlOperator($value, $operator);

		$builder->andWhere($sqlExpresssion . $operator . $modifier, $value);
	}

	/**
	 * Transforms orm order by expression and adds it to QueryBuilder.
	 * @param  string       $expression
	 * @param  string       $direction
	 * @param  QueryBuilder $builder
	 */
	public function processOrderByExpression($expression, $direction, QueryBuilder $builder)
	{
		list($chain, , $sourceEntity) = ConditionParserHelper::parseCondition($expression);
		$sqlExpression = $this->normalizeAndAddJoins($chain, $sourceEntity, $builder, $distinctNeeded);
		$builder->addOrderBy($sqlExpression . ($direction === ICollection::DESC ? ' DESC' : ''));

		if ($distinctNeeded) {
			throw new LogicException("Cannot order by '$expression' expression, includes has many relationship.");
		}
	}

	private function normalizeAndAddJoins(array $levels, $sourceEntity, QueryBuilder $builder, & $distinctNeeded = false, & $value = null, & $modifier = '%any')
	{
		$column = array_pop($levels);
		$sourceMapper = $this->mapper;
		$sourceAlias = $builder->getFromAlias();
		$sourceReflection = $sourceMapper->getStorageReflection();
		$sourceEntityMeta = $this->metadataStorage->get($sourceEntity ?: $sourceMapper->getRepository()->getEntityClassNames()[0]);

		foreach ($levels as $levelIndex => $level) {
			$property = $sourceEntityMeta->getProperty($level);
			if ($property->relationship === null) {
				throw new InvalidArgumentException("Entity {$sourceEntityMeta->className}::\${$level} does not contain a relationship.");
			}

			$targetMapper = $this->model->getRepository($property->relationship->repository)->getMapper();
			$targetReflection = $targetMapper->getStorageReflection();
			$targetEntityMetadata = $this->metadataStorage->get($property->relationship->entity);

			$relType = $property->relationship->type;
			if ($relType === Relationship::ONE_HAS_MANY || ($relType === Relationship::ONE_HAS_ONE && !$property->relationship->isMain)) {
				$targetColumn = $targetReflection->convertEntityToStorageKey($property->relationship->property);
				$sourceColumn = $sourceReflection->getStoragePrimaryKey()[0];
				$distinctNeeded = $relType === Relationship::ONE_HAS_MANY;

			} elseif ($relType === Relationship::MANY_HAS_MANY) {
				if ($property->relationship->isMain) {
					list($joinTable, list($inColumn, $outColumn)) = $sourceMapper->getManyHasManyParameters($property, $targetMapper);
				} else {
					$sourceProperty = $targetEntityMetadata->getProperty($property->relationship->property);
					list($joinTable, list($outColumn, $inColumn)) = $targetMapper->getManyHasManyParameters($sourceProperty, $sourceMapper);
				}

				$sourceColumn = $sourceReflection->getStoragePrimaryKey()[0];

				$builder->leftJoin(
					$sourceAlias,
					$joinTable,
					self::getAlias($joinTable),
					"[$sourceAlias.$sourceColumn] = [$joinTable.$inColumn]"
				);

				$sourceAlias = $joinTable;
				$sourceColumn = $outColumn;
				$targetColumn = $targetReflection->getStoragePrimaryKey()[0];
				$distinctNeeded = true;

			} else {
				$targetColumn = $targetReflection->getStoragePrimaryKey()[0];
				$sourceColumn = $sourceReflection->convertEntityToStorageKey($level);
			}

			$targetTable = $targetMapper->getTableName();
			$targetAlias = $level . str_repeat('_', $levelIndex);

			$builder->leftJoin(
				$sourceAlias,
				$targetTable,
				$targetAlias,
				"[$sourceAlias.$sourceColumn] = [$targetAlias.$targetColumn]"
			);

			$sourceAlias = $targetAlias;
			$sourceMapper = $targetMapper;
			$sourceReflection = $targetReflection;
			$sourceEntityMeta = $targetEntityMetadata;
		}

		$targetProperty = $sourceEntityMeta->getProperty($column);
		if ($targetProperty->isPrimary && $targetProperty->isVirtual) { // primary-proxy
			$primaryKey = $sourceEntityMeta->getPrimaryKey();

			if (count($primaryKey) > 1) { // composite primary key
				$modifier = '%any';
				list ($expression, $value) = $this->processMultiColumn($sourceReflection, $primaryKey, $value, $sourceAlias);
				return $expression;

			} else {
				$column = reset($primaryKey);
			}
		}

		list ($expression, $modifier, $value) = $this->processColumn($sourceReflection, $column, $value, $sourceAlias);
		return $expression;
	}

	public static function getAlias($name)
	{
		static $counter = 1;
		if (preg_match('#^([a-z0-9_]+\.){0,2}+([a-z0-9_]+?)$#i', $name, $m)) {
			return $m[2];
		}

		return '_join' . $counter++;
	}

	/**
	 * @param  mixed  $value
	 * @param  string $operator
	 * @return string
	 */
	protected function getSqlOperator($value, $operator)
	{
		if ($operator === ConditionParserHelper::OPERATOR_EQUAL) {
			if (is_array($value)) {
				$operator = ' IN ';
				return $operator;
			} elseif ($value === null) {
				$operator = ' IS ';
				return $operator;
			} else {
				$operator = ' = ';
				return $operator;
			}

		} elseif ($operator === ConditionParserHelper::OPERATOR_NOT_EQUAL) {
			if (is_array($value)) {
				$operator = ' NOT IN ';
				return $operator;
			} elseif ($value === null) {
				$operator = ' IS NOT ';
				return $operator;
			} else {
				$operator = ' != ';
				return $operator;
			}

		} else {
			$operator = " $operator ";
			return $operator;
		}
	}

	private function processColumn(IStorageReflection $sourceReflection, $column, $value, $sourceAlias)
	{
		$converted = $sourceReflection->convertEntityToStorage([$column => $value]);
		$column = key($converted);

		if (($pos = strpos($column, '%')) !== false) {
			$modifier = substr($column, $pos);
			$column = substr($column, 0, $pos);
		} else {
			$modifier = '%any';
		}

		$value = current($converted);
		return [
			"[{$sourceAlias}.{$column}]",
			$modifier,
			$value,
		];
	}

	private function processMultiColumn(IStorageReflection $sourceReflection, array $primaryKey, $value, $sourceAlias)
	{
		$pair = [];
		foreach ($primaryKey as $column) {
			$column = $sourceReflection->convertEntityToStorageKey($column);
			$pair[] = "[{$sourceAlias}.{$column}]";
		}
		if (!isset($value[0][0])) {
			$value = [$value];
		}
		return [
			'(' . implode(', ', $pair) . ')',
			$value,
		];
	}
}


code:
<?php

/**
 * This file is part of the Nextras\Orm library.
 * @license    MIT
 * @link       https://github.com/nextras/orm
 */

namespace Nextras\Orm\Mapper\Dbal;

use Nette\Object;
use Nextras\Dbal\Connection;
use Nextras\Dbal\QueryBuilder\QueryBuilder;
use Nextras\Orm\Collection\EntityIterator;
use Nextras\Orm\Collection\ICollection;
use Nextras\Orm\Collection\IEntityIterator;
use Nextras\Orm\Entity\IEntity;
use Nextras\Orm\Entity\Reflection\PropertyMetadata;
use Nextras\Orm\LogicException;
use Nextras\Orm\Mapper\IRelationshipMapperManyHasMany;
use Nextras\Orm\Repository\IRepository;

class RelationshipMapperManyHasMany extends Object implements IRelationshipMapperManyHasMany
{
	/** @var Connection */
	protected $connection;

	/** @var DbalMapper */
	protected $mapperOne;

	/** @var DbalMapper */
	protected $mapperTwo;

	/** @var PropertyMetadata */
	protected $metadata;

	/** @var IEntityIterator[] */
	protected $cacheEntityIterator;

	/** @var int[] */
	protected $cacheCounts;

	/** @var string */
	protected $joinTable;

	/** @var string */
	protected $primaryKeyFrom;

	/** @var string */
	protected $primaryKeyTo;

	/** @var IRepository */
	protected $targetRepository;

	public function __construct(Connection $connection, DbalMapper $mapperOne, DbalMapper $mapperTwo, PropertyMetadata $metadata)
	{
		$this->connection = $connection;
		$this->mapperOne = $mapperOne;
		$this->mapperTwo = $mapperTwo;
		$this->metadata = $metadata;

		$parameters = $mapperOne->getManyHasManyParameters($metadata, $mapperTwo);
		$this->joinTable = $parameters[0];

		if ($this->metadata->relationship->isMain) {
			$this->targetRepository = $this->mapperTwo->getRepository();
			list($this->primaryKeyFrom, $this->primaryKeyTo) = $parameters[1];
		} else {
			$this->targetRepository = $this->mapperOne->getRepository();
			list($this->primaryKeyTo, $this->primaryKeyFrom) = $parameters[1];
		}
	}

	// ==== ITERATOR ===================================================================================================

	public function getIterator(IEntity $parent, ICollection $collection)
	{
		/** @var IEntityIterator $iterator */
		$iterator = clone $this->execute($collection, $parent);
		$iterator->setDataIndex($parent->getValue('id'));
		return $iterator;
	}

	protected function execute(DbalCollection $collection, IEntity $parent)
	{
		$builder = $collection->getQueryBuilder();
		$preloadIterator = $parent->getPreloadContainer();
		$values = $preloadIterator ? $preloadIterator->getPreloadValues('id') : [$parent->getValue('id')];
		$cacheKey = $this->calculateCacheKey($builder, $values);

		$data = & $this->cacheEntityIterator[$cacheKey];
		if ($data !== null) {
			return $data;
		}

		$data = $this->fetchByTwoPassStrategy($builder, $values);
		return $data;
	}

	private function fetchByTwoPassStrategy(QueryBuilder $builder, array $values)
	{
		$sourceTable = $builder->getFromAlias();
		$targetTable = QueryBuilderHelper::getAlias($this->joinTable);

		$builder = clone $builder;
		$builder->leftJoin(
			$sourceTable,
			'%table',
			$targetTable,
			'%column = %column',
			// args
			$this->joinTable,
			"$targetTable.{$this->primaryKeyTo}",
			"{$sourceTable}." . $this->targetRepository->getMapper()->getStorageReflection()->getStoragePrimaryKey()[0]
		);
		$builder->addSelect('%column', "$targetTable.$this->primaryKeyTo");
		$builder->addSelect('%column', "$targetTable.$this->primaryKeyFrom");

		if ($builder->hasLimitOffsetClause()) { // todo !== 1
			$sqls = $args = [];
			foreach ($values as $value) {
				$builderPart = clone $builder;
				$builderPart->andWhere('%column = %any', "$targetTable.$this->primaryKeyFrom", $value);
				$sqls[] = $builderPart->getQuerySQL();
				$args = array_merge($args, $builderPart->getQueryParameters());
			}

			$query = '(' . implode(') UNION ALL (', $sqls) . ')';
			$result = $this->connection->queryArgs($query, $args);

		} else {
			$builder->andWhere('%column IN %any', "$targetTable.$this->primaryKeyFrom", $values);
			$result = $this->connection->queryArgs($builder->getQuerySQL(), $builder->getQueryParameters());
		}

		$values = [];
		foreach ($result as $row) {
			$values[$row->{$this->primaryKeyTo}] = null;
		}

		if (count($values) === 0) {
			return new EntityIterator([]);
		}

		$entitiesResult = $this->targetRepository->findBy(['id' => array_keys($values)]);
		$entities = $entitiesResult->fetchPairs('id', null);

		$grouped = [];
		foreach ($result as $row) {
			$grouped[$row->{$this->primaryKeyFrom}][] = $entities[$row->{$this->primaryKeyTo}];
		}

		return new EntityIterator($grouped);
	}

	// ==== ITERATOR COUNT =============================================================================================

	public function getIteratorCount(IEntity $parent, ICollection $collection)
	{
		$counts = $this->executeCounts($collection, $parent);
		$id = $parent->getValue('id');
		return isset($counts[$id]) ? $counts[$id] : 0;
	}

	protected function executeCounts(DbalCollection $collection, IEntity $parent)
	{
		$builder = $collection->getQueryBuilder();
		$preloadIterator = $parent->getPreloadContainer();
		$values = $preloadIterator ? $preloadIterator->getPreloadValues('id') : [$parent->getValue('id')];
		$cacheKey = $this->calculateCacheKey($builder, $values);

		$data = & $this->cacheCounts[$cacheKey];
		if ($data !== null) {
			return $data;
		}

		$data = $this->fetchCounts($builder, $values);
		return $data;
	}

	private function fetchCounts(QueryBuilder $builder, array $values)
	{
		$sourceTable = $builder->getFromAlias();
		$targetTable = QueryBuilderHelper::getAlias($this->joinTable);

		$builder = clone $builder;
		$builder->leftJoin(
			$sourceTable,
			'%table',
			$targetTable,
			'%column = %column',
			// args
			$this->joinTable,
			"$targetTable.{$this->primaryKeyTo}",
			"{$sourceTable}." . $this->targetRepository->getMapper()->getStorageReflection()->getStoragePrimaryKey()[0]
		);
		$builder->addSelect('%column', "$targetTable.$this->primaryKeyFrom");
		$builder->orderBy(null);

		if ($builder->hasLimitOffsetClause()) {
			$sqls = [];
			$args = [];
			foreach ($values as $value) {
				$build = clone $builder;
				$build->andWhere("%column = %any", $this->primaryKeyFrom, $value);
				$sqls[] = "SELECT %any AS %column, COUNT(*) AS [count] FROM (" . $build->getQuerySql() . ') [temp]';
				$args[] = $value;
				$args[] = $this->primaryKeyFrom;
				$args = array_merge($args, $build->getQueryParameters());
			}

			$sql = '(' . implode(') UNION ALL (', $sqls) . ')';
			$result = $this->connection->queryArgs($sql, $args);

		} else {
			$builder->addSelect('COUNT(%column) as count', $this->primaryKeyTo);
			$builder->andWhere('%column IN %any', $this->primaryKeyFrom, $values);
			$builder->groupBy('%column', $this->primaryKeyFrom);
			$result = $this->connection->queryArgs($builder->getQuerySql(), $builder->getQueryParameters());
		}

		$counts = [];
		foreach ($result as $row) {
			$counts[$row->{$this->primaryKeyFrom}] = $row->count;
		}
		return $counts;
	}

	// ==== OTHERS =====================================================================================================

	public function add(IEntity $parent, array $add)
	{
		if (!$add) {
			return;
		}

		$this->mapperOne->beginTransaction();
		$list = $this->buildList($parent, $add);
		$this->connection->query('INSERT INTO %table %values[]', $this->joinTable, $list);
	}

	public function remove(IEntity $parent, array $remove)
	{
		if (!$remove) {
			return;
		}

		$this->mapperOne->beginTransaction();
		$list = $this->buildList($parent, $remove);
		$this->connection->query(
			'DELETE FROM %table WHERE (%column[]) IN %any',
			$this->joinTable,
			array_keys(reset($list)),
			array_map('array_values', $list)
		);
	}

	protected function buildList(IEntity $parent, array $entries)
	{
		if (!$this->metadata->relationship->isMain) {
			throw new LogicException('ManyHasMany relationship has to be persisted in the primary mapper.');
		}

		$list = [];
		$primaryId = $parent->getValue('id');
		foreach ($entries as $id) {
			$list[] = [
				$this->primaryKeyFrom => $primaryId,
				$this->primaryKeyTo => $id,
			];
		}

		return $list;
	}

	protected function calculateCacheKey(QueryBuilder $builder, array $values)
	{
		return md5($builder->getQuerySql() . json_encode($builder->getQueryParameters()) . json_encode($values));
	}
}


code:
<?php

/**
 * This file is part of the Nextras\Orm library.
 * @license    MIT
 * @link       https://github.com/nextras/orm
 */

namespace Nextras\Orm\Mapper\Dbal;

use Nette\Object;
use Nextras\Dbal\Connection;
use Nextras\Dbal\QueryBuilder\QueryBuilder;
use Nextras\Orm\Collection\EntityContainer;
use Nextras\Orm\Collection\ICollection;
use Nextras\Orm\Entity\IEntity;
use Nextras\Orm\Entity\Reflection\PropertyMetadata;
use Nextras\Orm\Mapper\IMapper;
use Nextras\Orm\Mapper\IRelationshipMapper;
use Nextras\Orm\NotSupportedException;
use Nextras\Orm\Repository\IRepository;

class RelationshipMapperManyHasOne extends Object implements IRelationshipMapper
{
	/** @var Connection */
	protected $connection;

	/** @var PropertyMetadata */
	protected $metadata;

	/** @var IRepository */
	protected $targetRepository;

	/** @var EntityContainer[] */
	protected $cacheEntityContainers;

	public function __construct(Connection $connection, IMapper $targetMapper, PropertyMetadata $metadata)
	{
		$this->connection = $connection;
		$this->targetRepository = $targetMapper->getRepository();
		$this->metadata = $metadata;
	}

	public function getIterator(IEntity $parent, ICollection $collection)
	{
		$container = $this->execute($collection, $parent);
		return [$container->getEntity($parent->getRawValue($this->metadata->name))];
	}

	public function getIteratorCount(IEntity $parent, ICollection $collection)
	{
		throw new NotSupportedException();
	}

	protected function execute(DbalCollection $collection, IEntity $parent)
	{
		$builder = $collection->getQueryBuilder();
		$preloadContainer = $parent->getPreloadContainer();
		$values = $preloadContainer ? $preloadContainer->getPreloadValues($this->metadata->name) : [$parent->getRawValue($this->metadata->name)];
		$cacheKey = $this->calculateCacheKey($builder, $values);

		$data = & $this->cacheEntityContainers[$cacheKey];
		if ($data) {
			return $data;
		}

		$data = $this->fetch(clone $builder, stripos($cacheKey, 'JOIN') !== false, $values);
		return $data;
	}

	protected function fetch(QueryBuilder $builder, $hasJoin, array $values)
	{
		$values = array_values(array_unique(array_filter($values, function ($value) {
			return $value !== null;
		})));

		if (count($values) === 0) {
			return new EntityContainer([]);
		}

		$primaryKey = $this->targetRepository->getMapper()->getStorageReflection()->getStoragePrimaryKey()[0];
		$builder->andWhere('%column IN %any', $primaryKey, $values);
		$builder->addSelect(($hasJoin ? 'DISTINCT ' : '') . '%table.*', $builder->getFromAlias());
		$result = $this->connection->queryArgs($builder->getQuerySQL(), $builder->getQueryParameters());

		$entities = [];
		while (($data = $result->fetch())) {
			$entity = $this->targetRepository->hydrateEntity($data->toArray());
			$entities[$entity->getValue('id')] = $entity;
		}

		return new EntityContainer($entities);
	}

	protected function calculateCacheKey(QueryBuilder $builder, array $values)
	{
		return md5($builder->getQuerySQL() . json_encode($builder->getQueryParameters()) . json_encode($values));
	}
}


code:
<?php

/**
 * This file is part of the Nextras\Orm library.
 * @license    MIT
 * @link       https://github.com/nextras/orm
 */

namespace Nextras\Orm\Mapper\Dbal;

use Nette\Object;
use Nextras\Dbal\Connection;
use Nextras\Dbal\QueryBuilder\QueryBuilder;
use Nextras\Orm\Collection\EntityIterator;
use Nextras\Orm\Collection\ICollection;
use Nextras\Orm\Collection\IEntityIterator;
use Nextras\Orm\Entity\IEntity;
use Nextras\Orm\Entity\Reflection\PropertyMetadata;
use Nextras\Orm\Mapper\IRelationshipMapper;
use Nextras\Orm\Repository\IRepository;

class RelationshipMapperOneHasMany extends Object implements IRelationshipMapper
{
	/** @var Connection */
	protected $connection;

	/** @var PropertyMetadata */
	protected $metadata;

	/** @var DbalMapper */
	protected $targetMapper;

	/** @var IRepository */
	protected $targetRepository;

	/** @var string */
	protected $joinStorageKey;

	/** @var IEntityIterator[] */
	protected $cacheEntityIterator;

	/** @var int[] */
	protected $cacheCounts;

	public function __construct(Connection $connection, DbalMapper $targetMapper, PropertyMetadata $metadata)
	{
		$this->connection = $connection;
		$this->targetMapper = $targetMapper;
		$this->targetRepository = $targetMapper->getRepository();
		$this->metadata = $metadata;

		$this->joinStorageKey = $targetMapper->getStorageReflection()->convertEntityToStorageKey($this->metadata->relationship->property);
	}

	// ==== ITERATOR ===================================================================================================

	public function getIterator(IEntity $parent, ICollection $collection)
	{
		/** @var IEntityIterator $iterator */
		$iterator = clone $this->execute($collection, $parent);
		$iterator->setDataIndex($parent->getValue('id'));
		return $iterator;
	}

	protected function execute(DbalCollection $collection, IEntity $parent)
	{
		$builder = $collection->getQueryBuilder();
		$preloadIterator = $parent->getPreloadContainer();
		$values = $preloadIterator ? $preloadIterator->getPreloadValues('id') : [$parent->getValue('id')];
		$cacheKey = $this->calculateCacheKey($builder, $values);

		$data = & $this->cacheEntityIterator[$cacheKey];
		if ($data !== null) {
			return $data;
		}

		if ($builder->hasLimitOffsetClause() && count($values) > 1) {
			$data = $this->fetchByTwoPassStrategy($builder, $values);
		} else {
			$data = $this->fetchByOnePassStrategy($builder, stripos($cacheKey, 'JOIN') !== false, $values);
		}

		return $data;
	}

	protected function fetchByOnePassStrategy(QueryBuilder $builder, $hasJoin, array $values)
	{
		$builder = clone $builder;
		$builder->addSelect(($hasJoin ? 'DISTINCT ' : '') . '%table.*', $builder->getFromAlias());
		$builder->andWhere('%column IN %any', "{$builder->getFromAlias()}.{$this->joinStorageKey}", $values);
		return $this->queryAndFetchEntities($builder->getQuerySql(), $builder->getQueryParameters());
	}

	protected function fetchByTwoPassStrategy(QueryBuilder $builder, array $values)
	{
		$builder = clone $builder;
		$targetPrimaryKey = $this->targetMapper->getStorageReflection()->getStoragePrimaryKey();
		$isComposite = count($targetPrimaryKey) !== 1;

		foreach (array_unique(array_merge($targetPrimaryKey, [$this->joinStorageKey])) as $key) {
			$builder->addSelect("[$key]");
		}

		$sqls = $args = [];
		foreach ($values as $primaryValue) {
			$builderPart = clone $builder;
			$builderPart->andWhere("%column = %any", $this->joinStorageKey, $primaryValue);

			$sqls[] = $builderPart->getQuerySQL();
			$args = array_merge($args, $builderPart->getQueryParameters());
		}

		$query = '(' . implode(') UNION ALL (', $sqls) . ')';
		$result = $this->connection->queryArgs($query, $args);

		$map = $ids = [];
		if ($isComposite) {
			foreach ($result as $row) {
				$id = [];
				foreach ($targetPrimaryKey as $key) {
					$id[] = $row->{$key};
				}

				$ids[] = $id;
				$map[$row->{$this->joinStorageKey}][] = implode(',', $id);
			}

		} else {
			$targetPrimaryKey = $targetPrimaryKey[0];
			foreach ($result as $row) {
				$ids[] = $row->{$targetPrimaryKey};
				$map[$row->{$this->joinStorageKey}][] = $row->{$targetPrimaryKey};
			}
		}

		if (count($ids) === 0) {
			return new EntityIterator([]);
		}

		if ($isComposite) {
			$builder = $this->targetMapper->builder();
			$builder->andWhere('(%column[]) IN %any', $targetPrimaryKey, $ids);

			$entitiesResult = [];
			$collection = $this->targetMapper->toCollection($builder);
			foreach ($collection as $entity) {
				$entitiesResult[implode(',', $entity->getValue('id'))] = $entity;
			}
		} else {
			$entitiesResult = $this->targetRepository->findBy(['id' => $ids])->fetchPairs('id', null);
		}

		$entities = [];
		foreach ($map as $joiningStorageKey => $primaryValues) {
			foreach ($primaryValues as $primaryValue) {
				$entity = $entitiesResult[$primaryValue];
				$entities[$entity->getRawValue($this->metadata->relationship->property)][] = $entity;
			}
		}

		return new EntityIterator($entities);
	}

	private function queryAndFetchEntities($query, $args)
	{
		$result = $this->connection->queryArgs($query, $args);
		$entities = [];
		while (($data = $result->fetch())) {
			$entity = $this->targetRepository->hydrateEntity($data->toArray());
			$entities[$entity->getRawValue($this->metadata->relationship->property)][] = $entity;
		}

		return new EntityIterator($entities);
	}

	// ==== ITERATOR COUNT =============================================================================================

	public function getIteratorCount(IEntity $parent, ICollection $collection)
	{
		$counts = $this->executeCounts($collection, $parent);
		$id = $parent->getValue('id');
		return isset($counts[$id]) ? $counts[$id] : 0;
	}

	protected function executeCounts(DbalCollection $collection, IEntity $parent)
	{
		$builder = $collection->getQueryBuilder();
		$preloadIterator = $parent->getPreloadContainer();
		$values = $preloadIterator ? $preloadIterator->getPreloadValues('id') : [$parent->getValue('id')];
		$cacheKey = $this->calculateCacheKey($builder, $values);

		$data = & $this->cacheCounts[$cacheKey];
		if ($data !== null) {
			return $data;
		}

		$data = $this->fetchCounts($builder, $values);
		return $data;
	}

	private function fetchCounts(QueryBuilder $builder, array $values)
	{
		$targetStoragePrimaryKey = $this->targetMapper->getStorageReflection()->getStoragePrimaryKey()[0];
		$sourceTable = $builder->getFromAlias();

		$builder = clone $builder;
		$builder->addSelect('%column', "{$sourceTable}.{$this->joinStorageKey}");
		$builder->orderBy(null);

		if ($builder->hasLimitOffsetClause()) {
			$sqls = [];
			$args = [];
			foreach ($values as $value) {
				$build = clone $builder;
				$build->andWhere('%column = %any', "{$sourceTable}.{$this->joinStorageKey}", $value);
				$sqls[] = "SELECT %any AS %column, COUNT(*) AS [count] FROM (" . $build->getQuerySql() . ') [temp]';
				$args[] = $value;
				$args[] = $this->joinStorageKey;
				$args = array_merge($args, $build->getQueryParameters());
			}

			$sql = '(' . implode(') UNION ALL (', $sqls) . ')';
			$result = $this->connection->queryArgs($sql, $args);

		} else {
			$builder->addSelect('COUNT(%column) AS [count]', "{$sourceTable}.{$targetStoragePrimaryKey}");
			$builder->andWhere('%column IN %any', "{$sourceTable}.{$this->joinStorageKey}", $values);
			$builder->groupBy('%column', "{$sourceTable}.{$this->joinStorageKey}");
			$result = $this->connection->queryArgs($builder->getQuerySql(), $builder->getQueryParameters());
		}

		$counts = [];
		foreach ($result as $row) {
			$counts[$row->{$this->joinStorageKey}] = $row->count;
		}
		return $counts;
	}

	protected function calculateCacheKey(QueryBuilder $builder, array $values)
	{
		return md5($builder->getQuerySQL() . json_encode($builder->getQueryParameters()) . json_encode($values));
	}
}


code:
<?php

/**
 * This file is part of the Nextras\Orm library.
 * @license    MIT
 * @link       https://github.com/nextras/orm
 */

namespace Nextras\Orm\Mapper\Dbal;

use Nextras\Orm\Collection\ICollection;
use Nextras\Orm\Entity\IEntity;
use Nextras\Orm\NotSupportedException;

class RelationshipMapperOneHasOne extends RelationshipMapperOneHasMany
{
	public function getIterator(IEntity $parent, ICollection $collection)
	{
		return [parent::getIterator($parent, $collection)->current()];
	}

	public function getIteratorCount(IEntity $parent, ICollection $collection)
	{
		throw new NotSupportedException();
	}
}


code:
<?php

/**
 * This file is part of the Nextras\Orm library.
 * @license    MIT
 * @link       https://github.com/nextras/orm
 */

namespace Nextras\Orm\Mapper\Dbal\StorageReflection;

class CamelCaseStorageReflection extends StorageReflection
{
	protected function formatStorageKey($key)
	{
		return $key;
	}

	protected function formatEntityKey($key)
	{
		return $key;
	}

	protected function formatEntityForeignKey($key)
	{
		if (substr($key, -2) === 'Id') {
			$key = substr($key, 0, -2);
		}
		return $this->formatEntityKey($key);
	}
}