code:
<?php

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

namespace  Nextras\Dbal;

use Nextras\Dbal\Drivers\IDriver;

class SqlProcessor
{
	/** @var IDriver */
	private $driver;

	/** @var LazyHashMap */
	private $identifiers;

	/** @var array (name => [supports ?, supports [], expected type]) */
	protected $modifiers = [
		// expressions
		's' => [TRUE, TRUE, 'string'],
		'i' => [TRUE, TRUE, 'int'],
		'f' => [TRUE, TRUE, '(finite) float'],
		'b' => [TRUE, TRUE, 'bool'],
		'dt' => [TRUE, TRUE, 'DateTime'],
		'dts' => [TRUE, TRUE, 'DateTime'],
		'di' => [TRUE, TRUE, 'DateInterval'],
		'blob' => [TRUE, TRUE, 'blob string'],
		'_like' => [TRUE, FALSE, 'string'],
		'like_' => [TRUE, FALSE, 'string'],
		'_like_' => [TRUE, FALSE, 'string'],
		'any' => [FALSE, FALSE, 'pretty much anything'],
		'and' => [FALSE, FALSE, 'array'],
		'or' => [FALSE, FALSE, 'array'],

		// SQL constructs
		'table' => [FALSE, TRUE, 'string'],
		'column' => [FALSE, TRUE, 'string'],
		'values' => [FALSE, TRUE, 'array'],
		'set' => [FALSE, FALSE, 'array'],
		'raw' => [FALSE, FALSE, 'string'],
		'ex' => [FALSE, FALSE, 'array'],
	];

	public function __construct(IDriver $driver)
	{
		$this->driver = $driver;
		$this->identifiers = new LazyHashMap(function($key) {
			return $this->driver->convertIdentifierToSql($key);
		});
	}

	/**
	 * @param  mixed[] $args
	 * @return string
	 */
	public function process(array $args)
	{
		$last = count($args) - 1;
		$fragments = [];

		for ($i = 0, $j = 0; $j <= $last; $j++) {
			if (!is_string($args[$j])) {
				throw new InvalidArgumentException($j === 0
					? 'Query fragment must be string.'
					: "Redundant query parameter or missing modifier in query fragment '$args[$i]'."
				);
			}

			$i = $j;
			$fragments[] = preg_replace_callback(
				'#%(\??+\w++(?:\[\]){0,2}+)|(%%)|\[(.+?)\]#S', // %modifier | %% | [identifier]
				function ($matches) use ($args, &$j, $last) {
					if ($matches[1] !== '') {
						if ($j === $last) {
							throw new InvalidArgumentException("Missing query parameter for modifier $matches[0].");
						}
						return $this->processModifier($matches[1], $args[++$j]);

					} elseif ($matches[2] !== '') {
						return '%';

					} elseif (!ctype_digit($matches[3])) {
						return $this->identifiers->{$matches[3]};

					} else {
						return "[$matches[3]]";
					}
				},
				$args[$i]
			);

			if ($i === $j && $j !== $last) {
				throw new InvalidArgumentException("Redundant query parameter or missing modifier in query fragment '$args[$i]'.");
			}
		}

		return implode(' ', $fragments);
	}

	/**
	 * @param  string $type
	 * @param  mixed $value
	 * @return string
	 */
	public function processModifier($type, $value)
	{
		switch (gettype($value)) {
			case 'string':
				switch ($type) {
					case 'any':
					case 's':
					case '?s':
						return $this->driver->convertStringToSql($value);

					case 'i':
					case '?i':
						if (!preg_match('#^-?[1-9][0-9]*+\z#', $value)) {
							break;
						}
						return (string) $value;

					case '_like':
						return $this->driver->convertLikeToSql($value, -1);
					case 'like_':
						return $this->driver->convertLikeToSql($value, 1);
					case '_like_':
						return $this->driver->convertLikeToSql($value, 0);

					case 'column':
						if ($value === '*') {
							return '*';
						}
						// intential pass-through
					case 'table':
						return $this->identifiers->$value;

					case 'blob':
						return $this->driver->convertBlobToSql($value);

					case 'raw':
						return $value;
				}

			break;
			case 'integer':
				switch ($type) {
					case 'any':
					case 'i':
					case '?i':
						return (string) $value;
				}

			break;
			case 'double':
				if (is_finite($value)) { // database can not handle INF and NAN
					switch ($type) {
						case 'any':
						case 'f':
						case '?f':
							return ($tmp = json_encode($value)) . (strpos($tmp, '.') === FALSE ? '.0' : '');
					}
				}

			break;
			case 'boolean':
				switch ($type) {
					case 'any':
					case 'b':
					case '?b':
						return $this->driver->convertBoolToSql($value);
				}

			break;
			case 'NULL':
				switch ($type) {
					case 'any':
					case '?s':
					case '?i':
					case '?f':
					case '?b':
					case '?dt':
					case '?dts':
					case '?di':
					case '?blob':
						return 'NULL';
				}

			break;
			case 'object':
				if ($value instanceof \DateTimeImmutable || $value instanceof \DateTime) {
					switch ($type) {
						case 'any':
						case 'dt':
						case '?dt':
							return $this->driver->convertDateTimeToSql($value);

						case 'dts':
						case '?dts':
							return $this->driver->convertDateTimeSimpleToSql($value);
					}

				} elseif ($value instanceof \DateInterval) {
					switch ($type) {
						case 'any':
						case 'di':
						case '?di':
							return $this->driver->convertDateIntervalToSql($value);
					}

				} elseif (method_exists($value, '__toString')) {
					switch ($type) {
						case 'any':
						case 's':
						case '?s':
							return $this->driver->convertStringToSql((string) $value);

						case '_like':
							return $this->driver->convertLikeToSql($value, -1);
						case 'like_':
							return $this->driver->convertLikeToSql($value, 1);
						case '_like_':
							return $this->driver->convertLikeToSql($value, 0);
					}
				}

			break;
			case 'array':
				switch ($type) {
					// micro-optimizations
					case 'any':
						return $this->processArray("any[]", $value);

					case 'i[]':
						foreach ($value as $v) {
							if (!is_int($v)) break 2; // fallback to processArray
						}
						return '(' . implode(', ', $value) . ')';

					case 's[]':
						foreach ($value as &$subValue) {
							if (!is_string($subValue)) break 2; // fallback to processArray
							$subValue = $this->driver->convertStringToSql($subValue);
						}
						return '(' . implode(', ', $value) . ')';

					// normal
					case 'column[]':
					case 'table[]':
						$subType = substr($type, 0, -2);
						foreach ($value as &$subValue) {
							$subValue = $this->processModifier($subType, $subValue);
						}
						return implode(', ', $value);

					case 'and':
					case 'or':
						return $this->processWhere($type, $value);

					case 'values':
						return $this->processValues($type, $value);

					case 'values[]':
						return $this->processMultiValues($type, $value);

					case 'set':
						return $this->processSet($type, $value);

					case 'ex':
						return $this->process($value);
				}

				if (substr($type, -1) === ']') {
					$baseType = trim($type, '[]?');
					if (isset($this->modifiers[$baseType]) && $this->modifiers[$baseType][1]) {
						return $this->processArray($type, $value);
					}
				}
		}

		$baseType = trim($type, '[]?');
		$typeNullable = $type[0] === '?';
		$typeArray = substr($type, -2) === '[]';
		if (!isset($this->modifiers[$baseType])) {
			throw new InvalidArgumentException("Unknown modifier %$type.");

		} elseif (($typeNullable && !$this->modifiers[$baseType][0]) || ($typeArray && !$this->modifiers[$baseType][1])) {
			throw new InvalidArgumentException("Modifier %$baseType does not have %$type variant.");

		} elseif ($typeArray) {
			$this->throwInvalidValueTypeException($type, $value, 'array');

		} elseif ($value === NULL && !$typeNullable && $this->modifiers[$baseType][0]) {
			$this->throwWrongModifierException($type, $value, "?$type");

		} elseif (is_array($value) && !$typeArray && $this->modifiers[$baseType][1]) {
			$this->throwWrongModifierException($type, $value, "{$type}[]");

		} else {
			$this->throwInvalidValueTypeException($type, $value, $this->modifiers[$baseType][2]);
		}
	}

	/**
	 * @param  string $type
	 * @param  mixed $value
	 * @param  string $expectedType
	 * @return void
	 */
	protected function throwInvalidValueTypeException($type, $value, $expectedType)
	{
		$actualType = $this->getVariableTypeName($value);
		throw new InvalidArgumentException("Modifier %$type expects value to be $expectedType, $actualType given.");
	}

	/**
	 * @param  string $type
	 * @param  mixed $value
	 * @param  string $hint
	 * @return void
	 */
	protected function throwWrongModifierException($type, $value, $hint)
	{
		$valueLabel = is_scalar($value) ? var_export($value, TRUE) : gettype($value);
		throw new InvalidArgumentException("Modifier %$type does not allow $valueLabel value, use modifier %$hint instead.");
	}

	/**
	 * @param  string $type
	 * @param  array  $value
	 * @return string
	 */
	protected function processArray($type, array $value)
	{
		$subType = substr($type, 0, -2);
		foreach ($value as &$subValue) {
			$subValue = $this->processModifier($subType, $subValue);
		}

		return '(' . implode(', ', $value) . ')';
	}

	/**
	 * @param  string $type
	 * @param  array $value
	 * @return string
	 */
	protected function processSet($type, array $value)
	{
		$values = [];
		foreach ($value as $_key => $val) {
			$key = explode('%', $_key, 2);
			$column = $this->identifiers->{$key{0}};
			$expr = $this->processModifier(isset($key[1]) ? $key[1] : 'any', $val);
			$values[] = "$column = $expr";
		}

		return implode(', ', $values);
	}

	/**
	 * @param  string $type
	 * @param  array $value
	 * @return string
	 */
	protected function processMultiValues($type, array $value)
	{
		if (empty($value)) {
			throw new InvalidArgumentException('Modifier %values[] must contain at least one array element.');
		} elseif (empty($value[0])) {
			return 'VALUES ' . str_repeat('(DEFAULT), ', count($value) - 1) . '(DEFAULT)';
		}

		$keys = $values = [];
		foreach (array_keys($value[0]) as $key) {
			$keys[] = $this->identifiers->{explode('%', $key, 2)[0]};
		}
		foreach ($value as $subValue) {
			$subValues = [];
			foreach ($subValue as $_key => $val) {
				$key = explode('%', $_key, 2);
				$subValues[] = $this->processModifier(isset($key[1]) ? $key[1] : 'any', $val);
			}
			$values[] = '(' . implode(', ', $subValues) . ')';
		}

		return '(' . implode(', ', $keys) . ') VALUES ' . implode(', ', $values);
	}

	/**
	 * @param  string $type
	 * @param  array $value
	 * @return string
	 */
	private function processValues($type, array $value)
	{
		if (empty($value)) {
			return 'VALUES (DEFAULT)';
		}

		$keys = $values = [];
		foreach ($value as $_key => $val) {
			$key = explode('%', $_key, 2);
			$keys[] = $this->identifiers->{$key[0]};
			$values[] = $this->processModifier(isset($key[1]) ? $key[1] : 'any', $val);
		}

		return '(' . implode(', ', $keys) . ') VALUES (' . implode(', ', $values) . ')';
	}

	/**
	 * @param  string $type
	 * @param  array $value
	 * @return string
	 */
	private function processWhere($type, array $value)
	{
		if (count($value) === 0) {
			return '1=1';
		}

		$operands = [];
		foreach ($value as $_key => $subValue) {
			if (is_int($_key)) {
				if (!is_array($subValue)) {
					$subValueType = $this->getVariableTypeName($subValue);
					throw new InvalidArgumentException("Modifier %$type requires items with numeric index to be array, $subValueType given.");
				}

				$operand = '(' . $this->process($subValue) . ')';

			} else {
				$key = explode('%', $_key, 2);
				$column = $this->identifiers->{$key[0]};
				$subType = isset($key[1]) ? $key[1] : 'any';

				if ($subValue === NULL) {
					$op = ' IS ';
				} elseif (is_array($subValue) && $subType !== 'ex') {
					$op = ' IN ';
				} else {
					$op = ' = ';
				}

				$operand = $column . $op . $this->processModifier($subType, $subValue);
			}

			$operands[] = $operand;
		}

		return implode($type === 'and' ? ' AND ' : ' OR ', $operands);
	}

	/**
	 * @param  $value
	 * @return float|string
	 */
	protected function getVariableTypeName($value)
	{
		return is_object($value) ? get_class($value) : (is_float($value) && !is_finite($value) ? $value : gettype($value));
	}
}


code:
<?php

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

namespace Nextras\Dbal\Utils;

class DateTime extends \DateTime
{
	public function __toString()
	{
		return $this->format('c');
	}

	public function setTimestamp($timestamp)
	{
		$zone = $this->getTimezone();
		$this->__construct('@' . $timestamp);
		return $this->setTimeZone($zone);
	}
}

repo_name: Cerx/ToDo

license: mit


code:
<?php

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

namespace Nextras\Dbal\Utils;

use Nextras\Dbal\Connection;
use Nextras\Dbal\Drivers\Mysqli\MysqliDriver;
use Nextras\Dbal\Drivers\Pgsql\PgsqlDriver;
use Nextras\Dbal\IOException;

class FileImporter
{
	/**
	 * Imports & executes queries from sql file.
	 * Code taken from Adminer (http://www.adminer.org) & modified,
	 * @author Jakub Vrána
	 * @author Jan Tvrdík
	 * @author Michael Moravec
	 * @author Jan Škrášek
	 * @license Apache License
	 *
	 * @param  Connection $connection
	 * @param  string $file path to imported file
	 * @return int number of executed queries
	 */
	public static function executeFile(Connection $connection, $file)
	{
		$query = @file_get_contents($file);
		if ($query === FALSE) {
			throw new IOException("Cannot open file '$file'.");
		}

		$delimiter = ';';
		$offset = $queries = 0;
		$space = "(?:\\s|/\\*.*\\*/|(?:#|-- )[^\\n]*\\n|--\\n)";

		$driver = $connection->getDriver();
		if ($driver instanceof MysqliDriver) {
			$parse = '[\'"[]|/\*|-- |$';
		} elseif ($driver instanceof PgsqlDriver) {
			$parse = '[\'"]|/\*|-- |$|\$[^$]*\$';
		} else { // general
			$parse = '[\'"`#]|/\*|-- |$';
		}

		while ($query != '') {
			if (!$offset && preg_match("~^{$space}*DELIMITER\\s+(\\S+)~i", $query, $match)) {
				$delimiter = $match[1];
				$query = substr($query, strlen($match[0]));

			} else {
				preg_match('(' . preg_quote($delimiter) . "\\s*|$parse)", $query, $match, PREG_OFFSET_CAPTURE, $offset); // should always match
				$found = $match[0][0];
				$offset = $match[0][1] + strlen($found);

				if (!$found && rtrim($query) === '') {
					break;
				}

				if (!$found || rtrim($found) == $delimiter) { // end of a query
					$q = substr($query, 0, $match[0][1]);

					$queries++;
					$connection->query('%raw', $q);

					$query = substr($query, $offset);
					$offset = 0;

				} else { // find matching quote or comment end
					while (preg_match('(' . ($found == '/*' ? '\*/' : ($found == '[' ? ']' : (preg_match('~^-- |^#~', $found) ? "\n" : preg_quote($found) . "|\\\\."))) . '|$)s', $query, $match, PREG_OFFSET_CAPTURE, $offset)) { //! respect sql_mode NO_BACKSLASH_ESCAPES
						$s = $match[0][0];
						$offset = $match[0][1] + strlen($s);
						if ($s[0] !== '\\') {
							break;
						}
					}
				}
			}
		}

		return $queries;
	}
}


code:
<?php

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

namespace Nextras\Dbal\Utils;

class PostgreArrayConverter
{
	/**
	 * Converts Postgre string to PHP.
	 * @param  string $string
	 * @return array|NULL
	 */
	public static function toPhp($string, $start = 0, & $end = NULL)
	{
		if (empty($string) || $string[0] != '{') {
			return NULL;
		}

		$return = [];
		$inString = FALSE;
		$quote = '';
		$len = strlen($string);
		$value = '';

		for ($i = $start + 1; $i < $len; $i++) {
			$char = $string[$i];

			if ($inString === FALSE) {
				if ($char === '}' || $char === ',') {
					if ($char === '}' && $value === '' && empty($return)) {
						// skip empty values only when it is an empty object
						// e.g.: {} results in [], {,} results in ['', '']
					} else {
						if (ctype_digit($value)) {
							$value = (int) $value;
						} elseif (strcasecmp($value, 'null') === 0) {
							$value = NULL;
						}
						$return[] = $value;
						$value = '';
					}
					if ($char === '}') {
						$end = $i;
						break;
					}

				} elseif ($char === '{') {
					$value = self::toPhp($string, $i, $i);

				} elseif ($char === '"' || $char === "'") {
					$inString = TRUE;
					$quote = $char;

				} else {
					$value .= $char;
				}

			} elseif ($char === $quote) {
				if ($string[$i - 1] == "\\") {
					$value = substr($value, 0, -1) . $char;
				} else {
					$inString = FALSE;
				}

			} else {
				$value .= $char;
			}
		}

		return $return;
	}

	/**
	 * Converts PHP to Postgre string.
	 * @param  array|NULL $array
	 * @return string
	 */
	public static function toSql($array)
	{
		if ($array === NULL) {
			return '';
		}

		settype($array, 'array'); // can be called with a scalar or array
		$result = [];
		foreach ($array as $item) {
			if (is_array($item)) {
				$result[] = self::toSql($item);

			} else {
				if ($item === NULL) {
					$item = 'NULL';
				} elseif (!is_int($item)) { // quote only non-numeric values
					$item = '"' . str_replace('"', '\\"', $item) . '"'; // escape double quote
				}
				$result[] = $item;
			}
		}

		return '{' . implode(',', $result) . '}';
	}
}


code:
<?php

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

namespace Nextras\Dbal\Utils;

class Typos
{
	/**
	 * Returns the closest word to the $current word which is not farther than $maxDistance
	 * or NULL if such word does not exist.
	 *
	 * @param  string $current
	 * @param  string[] $words
	 * @param  int $maxDistance
	 * @return string|NULL
	 */
	public static function getClosest($current, array $words, $maxDistance)
	{
		$shortest = $maxDistance + 1;
		$closest = NULL;
		foreach ($words as $word) {
			$distance = levenshtein($current, $word);
			if ($distance < $shortest) {
				$closest = $word;
			}
		}
		return $closest;
	}
}


code:
<?php

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

namespace Nextras\Dbal;

use Exception;

class InvalidArgumentException extends \InvalidArgumentException
{
}

class InvalidStateException extends \LogicException
{
}

class IOException extends \RuntimeException
{
}

class NotImplementedException extends \LogicException
{
}

class NotSupportedException extends \LogicException
{
}

class ConnectionException extends DriverException
{
}

class DriverException extends Exception
{
	/** @var int */
	private $errorCode;

	/** @var string */
	private $errorSqlState;

	/**
	 * @param  string $message
	 * @param  int $errorCode
	 * @param  string $errorSqlState
	 * @param  Exception $previousException
	 */
	public function __construct($message, $errorCode = 0, $errorSqlState = '', Exception $previousException = NULL)
	{
		parent::__construct($message, 0, $previousException);
		$this->errorCode = (int) $errorCode;
		$this->errorSqlState = $errorSqlState;
	}

	public function getErrorCode()
	{
		return $this->errorCode;
	}

	public function getErrorSqlState()
	{
		return $this->errorSqlState;
	}

}

class QueryException extends DriverException
{
	/** @var string */
	private $sqlQuery;

	public function __construct($message, $errorCode = 0, $errorSqlState = '', $previousException = NULL, $sqlQuery = NULL)
	{
		parent::__construct($message, $errorCode, $errorSqlState, $previousException);
		$this->sqlQuery = (string) $sqlQuery;
	}

	public function getSqlQuery()
	{
		return $this->sqlQuery;
	}

}

abstract class ConstraintViolationException extends QueryException
{
}

class NotNullConstraintViolationException extends ConstraintViolationException
{
}

class ForeignKeyConstraintViolationException extends ConstraintViolationException
{
}

class UniqueConstraintViolationException extends ConstraintViolationException
{
}


code:
language: php
php:
    - 5.5
    - 5.6
    - 7.0
    - hhvm

matrix:
    fast_finish: true

sudo: false

cache:
    directories:
        - $HOME/.composer/cache
        - vendor

before_script:
    # Create php.ini & sections.ini
    - cp ./tests/config.sample.neon ./tests/config.neon
    - cp ./tests/php-unix.ini ./tests/php.ini

    - if [ "$TRAVIS_PHP_VERSION" == "7.0" ]; then cat ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini >> ./tests/php.ini; fi
    - if [ "$TRAVIS_PHP_VERSION" == "7.0" ]; then NTESTER_FLAGS="--coverage ./coverage.xml --coverage-src ./src"; else TESTER_FLAGS=""; fi

    - if [ "$TRAVIS_PHP_VERSION" == "hhvm" ]; then cp ./tests/sections.hhvm.ini ./tests/sections.ini; fi
    - if ! [ "$TRAVIS_PHP_VERSION" == "hhvm" ]; then cp ./tests/sections.sample.ini ./tests/sections.ini; fi

    # Create MySQL & Postgre database
    - psql -c 'CREATE DATABASE nextras_orm_test' -U postgres
    - mysql -e 'CREATE DATABASE nextras_orm_test;'

    - mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -u root mysql

    # Install Nette Tester
    - if ! [ "$TRAVIS_PHP_VERSION" == "hhvm" ]; then phpenv config-rm xdebug.ini; fi
    - if [ "$TRAVIS_PHP_VERSION" == "7.0" ]; then composer require --dev jakub-onderka/php-parallel-lint:~0.9.2; fi
    - travis_retry composer update --no-interaction --prefer-source

script:
    - ./tests/run.sh -s $NTESTER_FLAGS ./tests/cases
    - if [ "$TRAVIS_PHP_VERSION" == "7.0" ]; then vendor/bin/parallel-lint src tests/cases; fi

after_script:
    - if [ "$TRAVIS_PHP_VERSION" == "7.0" ]; then
      wget https://github.com/satooshi/php-coveralls/releases/download/v1.0.1/coveralls.phar
      && php coveralls.phar --verbose --config tests/.coveralls.yml
      || true;
      fi

after_failure:
    # Print *.actual content & log content
    - for i in $(find tests -name \*.actual); do echo "--- $i"; cat $i; echo; echo; done
    - for i in $(find tests -name \*.log); do echo "--- $i"; cat $i; echo; echo; done


nextras/orm

type: library

description: Nextras Orm framework

keywords: orm database nextras

license: MIT

Nextras\Orm\: src/

classmap: tests/cases tests/inc


code:
Collection
##########

Each entity selection is returned as a `Nextras\Orm\Collection\ICollection` instance. `ICollection` implements `\Traversable` interface and allows you to work with entity list with additional api.

/--div .[advice]
In Orm, we use coding standard which assumes, that
- `get*` methods return IEntity instance or null,
- `find*` methods return ICollection instance.
\--
<br>

All methods, which modify collection, treat the collection as **immutable** and return new `ICollection` instance. Available methods:

- `findBy(array $conds)` - applies another filtering,
- `getBy(array $conds)` - applies another filtering and returns the first result row,
- `orderBy($column, $direction)` - sorts the result set by the provided column,
- `resetOrderBy()` - removes all defined sorting columns,
- `limitBy($limit, $offset)` - limits the collection, optionally sets the starting offset,
- `fetch()` - returns the unfetched result row, repeated calls iterate over the whole result set,
- `fetchAll()` - returns the whole result set as array,
- `fetchPairs($key, $value)` - process the whole result set and returns it as array. The first argument accepts the column which will be used as array key. If null is provided, result array will be indexed naturally. The second argument accepts the column which will be used as array value. If null is provided, the whole entity will be provided as the value.

/--php
// all book entities indexed by their primary key
$orm->books
	->findAll()
	->fetchPairs('id', null);

// all book titles sorted backward and naturally indexed
$orm->books
	->findAll()
	->orderBy('title', ICollection::DESC)
	->fetchPairs(null, 'title');
\--

Filtering
=========

Each collection can be filtered by array of conditions. These are passed as the first parameter of `findBy()` method. Array consists of entity property names and values. Keys can contain optional operator. Default operator is equality. Let's see the example.

/--php
$books = $orm->books->findBy([
	'author' => $author->id,
	'publishedAt<=' => new DateTime(),
]);
\--

Allowed operators are `=`, `!=`, `<=`, `<`, `>=` and `>`.

You can filter your collection using conditions based on related entities. To filter by their value, use traversing expression: it consists of the path delimited by `->` arrow. You have to start the expression by referencing the starting point using `this` keyword.

/--php
// find all books which were authored by Jon Snow
$orm->books->findBy(['this->author->name' => 'Jon Snow']);

// find all books which were not translated by Jon Snow
$orm->books->findBy(['this->translator->name!=' => 'Jon Snow']);
\--

There are few restrictions:
- Filtering does not implement any support for aggregation. If you need some more complex queries, proxy your methods to mapper layer. See more in [Repository chapter | repository].
- Traversing is currently supported only over the persisted properties. Your own virtual properties have not been implemented yet.

--------------

Sorting
=======

You can easily sort your collection. Use `orderBy()` method, which accepts the column name and the order direction. By default, columns are sorted in an ascending order. To change the order, use `ICollection::ASC` or `ICollection::DESC` constants.

/--php
$orm->books->findAll()->orderBy('title', ICollection::DESC);
\--

You can add more ordering rules, which will be used when the earlier defined ordering columns are same. The sorting may be reset by `resetOrderBy()` method.

--------------

Limiting
========

To limit your data collection, just use `limitBy()` method.

/--php
// get the last 10 published books
$orm->books->findAll()->orderBy('publishedAt', ICollection::DESC)->limitBy(10);

// get the 10 penultimate published books
$orm->books->findAll()->orderBy('publishedAt', ICollection::DESC)->limitBy(10, 10);
\--

---------------

Counting
========

It is easy to count entities in a collection. There are two methods:
- `count()` fetches the entities from the storage and counts them,
- `countStored()` asks the storage for the collection count, the implementation depends on the mapper layer, basically the `countStored()` method runs own SQL query.

The `count()` method is quite useful if you know that you will need entities in the collection. The `countStored()` is needed if you do a pagination.

/--php
public function renderArticles($categoryId)
{
	$articles = $this->orm->articles->findBy(['category' => $categoryId]);

	$limit = 10;
	$offset = $this->page * 10;

	$this->paginator->totalCount = $articles->countStored();
	$this->template->articles = $articles->limitBy($limit, $offset);
}
\--
/--html
{if $articles->count()}
	{foreach $articles} ... {/foreach}
{else}
	You have no articles.
{/if}
\--


code:
Conventions
###########

The database naming conventions shouldn't affect your php naming conventions. Orm is designed to help you not to bother with your database relics. In Nextras\Orm, classes taking care about conventions are placed in `StorageReflection` namespace.

Table name
----------

Table names are directly resolved in the mapper, they are derived from the mapper class name. By default, the names are created as underscored name of the mapper class with stripped "Mapper" suffix, eg. `EventsMapper` -> `events`.

If you would like to force some other table name, define `$tableName` property, or override `getTableName()` method in your mapper class.

/--php
use Nextras\Orm\Mapper\Mapper;

class EventsMapper extends Mapper
{
	protected $tableName = 'events';

	// or

	protected function getTableName()
	{
		return 'blog_events';
	}
}
\--

Properties
----------

StorageReflection takes care about converting column names. Conventions are represented by interface `Nextras\Orm\StorageReflection\IStorageReflection`. However, Nette mapper requires slightly advanced `IDbStorageReflection`, which is introduced because of joining table in many to many pattern.

Orm comes with two predefined reflections implementing `IDbStorageReflection`:
- CamelCaseDbStorageReflection
- UnderscoredDbStorageReflection

These predefined classes assume "camelCase" naming in Orm layer, and transform it for the database layer. (In fact, CamelCase reflection does no transform in column naming.)

- If database column has `_id` (or `Id`) suffix and is defined as a foreign key, they automatically strip it.
- If database table has only one primary column, it's automatically mapped to primary property in Orm (`$id`).

However, you are free to add your own mapping. Just call `addMapping($entityName, $storageName)` method. The right way to do this is to inherit `createStorageReflection()` method in your mapper class.

/--code php
use Nextras\Orm\Mapper\Mapper;

class EventsMapper extends Mapper
{
	protected function createStorageReflection()
	{
		$reflection = parent::createStorageReflection();
		$reflection->addMapping('entityProperty', 'database_property');
		return $reflection;
	}
}
\--

Properties' converters
----------------------

StorageReflection offers API for data transformation when they are passed from storage to PHP and otherwise. The aforementioned `addMapping($entityName, $storageName, toEntityCb, $toStorageCb)` method has two optional parameters that accept callbacks. These callbacks receive the value and key parameters and must return the new converted value. The first callback is for conversion from the storage to PHP, the second is for conversion from PHP to the storage. Let's see an example:

/--code php
/**
 * @param bool $isPublic
 */
class File extends Nextras\Orm\Entity\Entity
{
}

class FilesMapper extends Nextras\Orm\Mapper\Dbal\DbalMapper
{
    protected function createStorageReflection()
    {
        $reflection = parent::createStorageReflection();
        $reflection->addMapping('isPublic', 'is_public', function ($val) {
            return $val === 'Y' || $val === 'y';
        }, function ($val) {
            return $val ? 'Y' : 'N';
        });
        return $reflection;
    }
}
\--

Properties' modifiers for Nextras Dbal
--------------------------------------

The underlying layer Nextras Dbal takes care about converting and sanitizing the values for SQL INSERT/UPDATE query. By default, the `%any` modifier is used and values is trasformed by its type. However, you may want to force different behavior and modifier for Nextras Dbal layer. To do that, use `addModifier($storageKey, $modifier)` method, which accepts the table's column name and Dbal's modifier. Let's see an example:

/--code php
/**
 * @param string $contents
 */
class File extends Nextras\Orm\Entity\Entity
{
}

class FilesMapper extends Nextras\Orm\Mapper\Dbal\DbalMapper
{
    protected function createStorageReflection()
    {
        $reflection = parent::createStorageReflection();
        $reflection->addModifier('contents', '%blob');
        return $reflection;
    }
}
\--

HasMany joining table
---------------------

There are many possibilities to change default table joining conventions. If you are using `m:n`, you can change its pattern property. By default the pattern is defined as `%s_x_%s`. The first placeholder is the primary table name.

/--code php
use Nextras\Orm\Mapper\Mapper;

class BaseMapper extends Mapper
{
	protected function createStorageReflection()
	{
		$reflection = parent::createStorageReflection();
		$reflection->manyHasManyStorageNamePattern = '%s_2_%s';
		return $reflection;
	}
}
\--

If you need more advanced changes, feel free to override `getManyHasManyParameters()` method in your mapper. This method should return array, where the first value is the joining table name, the second is the array of joining keys/columns. If you have only one `m:n` relationship between two entities, you can return result only based on the passed target mapper, source property's metadata are available for more detailed matching.

/--code php
use Nextras\Orm\Mapper\Mapper;

class EmployeesMapper extends Mapper
{
	public function getManyHasManyParameters(PropertyMetadata $sourceProperty, IMapper $targetMapper)
	{
		if ($targetMapper instanceof DepartmentsMapper) {
			return ['emp_dept', ['emp_no', 'dept_no']];
		}
		return parent::getManyHasManyParameters($sourceProperty, $targetMapper);
	}
}
\--


code:
Nextras\Orm
###########

Nextras\Orm is the next generation Orm designed for efficiency and simple usage. It is highly customizable, you can create your own mapper layer to use your favorite storage.

Code example
===========

/--php
$author = new Author();
$author->name = 'Jon Snow';
$author->born = 'yesterday';
$author->mail = 'snow@wall.st';

$publisher = new Publisher();
$publisher->name = '7K publisher';

$book = new Book();
$book->title = 'My Life on The Wall';
$book->author = $author;
$book->publisher = $publisher;

$orm->books->persistAndFlush($book);
\--

Calling `persistAndFlush()` on `$book` recursively persists author, publisher and book.

Performance
===========

Nextras\Orm uses constant number of queries: it does not matter, how much data you will ouput or how many inner cycles you will use. Orm will fetch all needed data in advance. Let's see an exmaple:

/--php
$authors = $orm->authors->findAll();
foreach ($authors as $author) {
	echo $author->name;

	foreach ($author->books as $book) {
		echo $book->title;
		echo $book->translator->name;

		foreach ($book->tags as $tag) {
			echo $tag->name;
		}
	}
}
\--

The code above will run 4 queries:
1. to select all the authors,
2. to select all books which were authored by previously selected authors,
3. to select all translators who translated the previously selected books,
4. to select all tags for previously selected books.

As you can see, Orm will not query in each cycle pass, rather it will query all the needed data at once.


code:
Entity
######

Entity is data crate which, basically, contains data for one table row. Each entity has to implement `Nextras\Orm\Entity\IEntity` interface. Orm has predefined class `Nextras\Orm\Entity\Entity`, which implements the interface and provides other useful features and proper integration with the library.

Data are accessible as properties. You have to define all properties that should be available. Properties are defined by Phpdoc annotations. Let's start with a basic entity:

/--php
/**
 * @property int         $id      {primary}
 * @property string      $name
 * @property DateTime    $born
 * @property string|null $web
 *
 * @property-read int    $age
 */
class Member extends Nextras\Orm\Entity\Entity
{
}
\--

Phpdoc property definition consists of its type and name. If you would like to use read-only property, define it with `@property-read` annotation; such annotation is useful to define properties which are based on values of other properties. Properties could be optional/nullable; to do that, just provide another type - `null`.

If you put some value into the property, the value will be validated by property type annotation. It's type casting is performed if it is possible and safe. Supported types are `null`, `string`, `int`, `float`, `array`, `mixed` and object types. Validation is provided on all properties, except for properties defined with container - in that case validation should do the container.

"Property access" is the easiest way to work with the data, although, such feature is not defined in `IEntity` interface. To conform the interface, you can use "method access": `getValue()` method for reading, `setValue()` method for setting a property, `hasValue()`, etc. There is a special `getRawValue()` method, which returns raw representation of the value. THe raw representation is basically the stored value (a primary key for relationship property).

/--php
$member = new Member();

$member->name = 'Jon';
$member->setValue('name', 'Jon');

echo $member->name;
echo $member->getValue('name');

echo isset($member->web) ? 'has web' : '-';
echo $member->hasValue('web') ? 'has web' : '-';

$member->isPersisted(); // false
\--

Attaching entities to the repository is letting Orm know about your entities, it does not store the entity. Attaching to repository injects the required dependencies into your entity (through inject property annotations or inject methods). If you need some dependency before attaching entity to the repository, feel free to pass the dependency through the constructor, which is by default empty.

Each entity can be created "manually". Entities can be simply connected together. Let's see an example:

/--php
$author = new Author();

$book = new Book();
$book->author = $author;
$book->tags->set([new Tag(), new Tag()]);
\--

If a instance Author is attached to the repository, all other new connected entities are automatically attached to their repositories too. See more in [relationships chapter | relationships].

-----------

Getters and setters
===================

Entity allows you to implement own getters and setter to modify the passed value. These methods are optional and should be defined as `protected`. The method name consists of the `getter` prefix and a property name, `setter` prefix respectively. You can define just one of them. Getters and setters are not supported for property containers, so that relationships cannot have them.

Getter method receives the stored value as the first parameter and should return the optionally modified value. "Virtual getters" do not receive any value. Setter method receives the user given value and should return the modified value for storing it in the entity.

/--php
/**
 * ...
 * @property string $name
 * @property int    $siblingsCount
 */
class FamilyMember extends Entity
{
	protected function getterName($name)
	{
		return ucwords($name);
	}

	protected function setterSiblingsCount($siblings)
	{
		return max((int) $siblings, 0);
	}
}
\--

-----------

Property modifiers
==================

Each property can be annotated with a modifier. Modifiers are optional and provide possibility to extend entity properties' behavior. Modifiers are written after the property name. Each modifier is surrounded by curly braces. The first compulsory token is the modifier name, other tokens are optional and depend on the specific modifier type. Orm comes with few predefined property modifiers:

- `{primary}`                                - marks the property; it is mapped to primary key.
- `{primary-proxy}`                          - marks the property to map the primary key; useful for easy access to composite primary key.
- `{enum self::TYPE_*}`                      - enables extended validation against values enumeration.
- `{default now}`                            - defines property default value.
- `{virtual}`                                - marks property as "do not persist in storage".
- `{container ContainerClassName}`           - sets property container/injection.
- `{1:m TargetEntity::$property}`            - see [relationships].
- `{m:1 TargetEntity::$property}`            - see [relationships].
- `{m:n TargetEntity::$property}`            - see [relationships].
- `{1:1 TargetEntity::$property}`            - see [relationships].

--------

{primary} and {primary-proxy}
---------

Each entity has to have defined the `$id` property.
By default, the `$id` property is the only primary key of the entity; the `$id` property is defined in `Nextras\Orm\Entity\Entity` class, but it is not marked as primary, because this is the default behavior, which can be changed by the `{primary}` modifier. By adding the modifier to property, you mark it as the new primary key. You can use the modifier multiple times to create a composite primary key. If the modifier is applied to a relationship property, the relationship's primary key is automatically used.

/--php
/**
 * @property int    $id       {primary}
 * @property string $name
 */
class Tag extends Nextras\Orm\Entity\Entity
{
}

/**
 * @property mixed  $id        {primary-proxy}
 * @property Tag    $tag       {m:1 Tag::$followers} {primary}
 * @property User   $follower  {m:1 User::$followedTags} {primary}
 */
class TagFollower extends Nextras\Orm\Entity\Entity
{
}

$tag = new Tag();
$tag->id = 1;

$user = new User();
$user->id = 2345;

$tagFollower = new TagFollower();
$tagFollower->tag = $tag;
$tagFollower->user = $user;

return $tagFollower->id;
// retruns array(1, 2345);
\--

{enum}
------

You can easily validate passed value by value enumeration. To set the enumeration validation, use `enum` modifier with the list of constants (separated by a space); or pass a constant name with a wildcard.

/--php
/**
 * ...
 * @property int $type {enum self::TYPE_*}
 */
class Event extends Nextras\Orm\Entity\Entity
{
	const TYPE_PUBLIC  = 0;
	const TYPE_PRIVATE = 1;
	const TYPE_ANOTHER = 2;
}
\--

--------

{default}
---------

You can easily set the default value. The default modifier also accepts a reference to constant.

/--php
/**
 * ...
 * @property string  $name   {default "Jon Snow"}
 * @property int     $type   {default self::TYPE_PUBLIC}
 */
class Event extends Nextras\Orm\Entity\Entity
{
	const TYPE_PUBLIC = 0;
}
\--

--------

{virtual}
---------

Virtual modifier marks specific property as virtual - such property won't be stored in the mapper; this modifier is useful to use with `property-read` annotation together.

/--php
/**
 * ...
 * @property      DateTime $born
 * @property-read int      $age    {virtual}
 */
class Member extends Nextras\Orm\Entity\Entity
{
	protected function getterAge()
	{
		return date('Y') - $this->born->format('Y');
	}
}

$member = new Member();
$member->born = new DateTime('2000-01-01');
echo $member->age;
\--

--------

{container} - property containers
---------------------------------

Containers encapsulate the property value. There are two basic types of containers:

- **IProperty** - basic container which implements `Nextras\Orm\Entity\IProperty` interface; reading and writing value do not call any special methods, retrieve the container object directly.
 *This feature is used in "has many" relationships. Reading the property value returns object container, which holds and directs the relationship.*

- **IPropertyContainer** - fully encapsulates the value; value is set by `setInjectedValue()` method, and retrieved by `getInjectedValue()` method.
 *This feature is used in "has one" relationships. Reading the property value calls `getInjectedValue()` method, writing some value internally calls `setInjectedValue()` method.*

Property containers are created by entity itself lazily (when needed).

----------

Entity dependencies
===================

Your entity can require some dependency to work. Orm comes with `Nextras\Orm\Repository\IDependencyProvider` interface, which takes care about injecting needed dependencies. If you use `OrmExtension` for `Nette\DI`, it will automatically call standard DI injections (injection methods and `@inject` annotation). Dependencies are injected when entity is attached to repository.

/--php
/**
 * ...
 */
class Book extends Nextras\Orm\Entity\Entity
{
	/** private EanSevice */
	private $eanService;

	public function injectEanService(EanService $service)
	{
		$this->eanService = $service;
	}
}
\--


code:
Events
######

ORM's events provides powerful extension points. Events are implemented in Nette Framework's way, just add your callback as another item of array. To bind your code to events indenpendently on their instantiation, use "Kdyby/Events":https://github.com/Kdyby/Events.

|* Callback registration        |* Callback signature
| ||
|* `Nextras\Orm\Model\Model`  ||
| `$onFlush` | `function (IEntity[] $persisted, IEntity[] $removed) {}`
| ||
|* `Nextras\Orm\Repository\Repository` ||
| `$onBeforePersist` | `function (IEntity $entity) {}`
| `$onAfterPersist`  | `function (IEntity $entity) {}`
| `$onBeforeInsert`  | `function (IEntity $entity) {}`
| `$onAfterInsert`   | `function (IEntity $entity) {}`
| `$onBeforeUpdate`  | `function (IEntity $entity) {}`
| `$onAfterUpdate`   | `function (IEntity $entity) {}`
| `$onBeforeRemove`  | `function (IEntity $entity) {}`
| `$onAfterRemove`   | `function (IEntity $entity) {}`
| `$onFlush`         | `function (IEntity[] $persisted, IEntity[] $removed) {}`

/--php
$orm->books->onBeforeInsert[] = function (Book $book) {
	echo "Inserting into DB " . $book->title;
};
\--

Entity events
-------------

You may react on events also inside your entity. To implement your code, override event method. Do not forget to call parent's implementation!

|* Method signature   |* Description
| `onCreate()`                                    | When new entity is created. (ie. onLoad is not called)
| `onLoad(array $data)`                           | When entity is loaded from DB. (ie. onCreate is not called)
| `onRefresh(array $data)`                        | When entity is refreshed from DB by a persist call.
| `onFree()`                                      | When all entites of model are destroyed.
| `onAttach(IRepository $r, EntityMetadata $m)`   | When attached to repository.
| `onDetach()`                                    | When detached from repository.
| `onPersist(mixed $id)`                          | When inseted/updated.
| `onBeforePersist()`                             | Before insert/update.
| `onAfterPersist()`                              | After insert/update.
| `onBeforeInsert()`                              | Before insert.
| `onAfterInsert()`                               | After insert.
| `onBeforeUpdate()`                              | Before update.
| `onAfterUpdate()`                               | After update.
| `onBeforeRemove()`                              | Before remove.
| `onAfterRemove()`                               | After remove.

/--php
/**
 * @property int $id
 * @property DateTime $createdAt
 */
class Book extends Nextras\Orm\Entity\Entity
{
	protected function onCreate()
	{
		parent::onCreate();
		$this->createdAt = strtotime('- 3 days');
	}
}
\--


code:
Mapper
######

Mapper is the last ORM layer which communicates with database. In contrary to repository, mapper is storage specific, even more, also database specific. Everything database specific should be implemented only in mapper layer. Orm comes with two predefined mappers: ArrayMapper, which works over PHP array, and DbalMapper, which uses "Nextras\Dbal":[1] database abstraction layer.

ArrayMapper
===========

Array mapper allows you to work with Orm without database. `ArrayMapper` can be heavily used in tests. Mocking repositories and entities is not so easy, therefore you can use `TestMapper`, which will allow you to pass orm dependencies as in production mode, but test will not require any type of database connection. All Orm integrations tests are also run with Test mappers storage.

Collection results form Array mapper are returned as `ArrayCollection` instance.

DbalMapper
==========

Dbal mapper uses "Nextras\Dbal":[1] layer. Dbal and Orm supports MySQL and PostgreSQL database (by `php_mysqli` and `php_pgsql` php extensions). Dbal mapper is aliased as `Nextras\Orm\Mapper\Mapper` class. Collection results from Dbal mapper are returned as `DbalCollection` (if you use Dbal's QueryBuilder) or `ArrayMapper` (if you use SQL query).

To set mapper's database **table name** set `$tableName` property or override `getTableName()` method.

/--php
class BooksMapper extends Nextras\Orm\Mapper\Mapper
{
	protected $tableName = 'tbl_book';

	// or

	public function getTableName()
	{
		return 'tbl_book';
	}
}
\--

It is impossible to filter data by "repository" API, you can write your own filtering into own methods in mapper. Dbal mapper allows you to return `Nextras\Dbal\QueryBuilder\QueryBuilder` or `Nextras\Dbal\Result\Result` object. Query builder instance will be injected into DbalCollection, Result instance will be executed and returned rows will be wrapped up in ArrayCollection. DbalCollection is lazy, it is always better to try use dbal's query builder.

You can get new query builder by calling `builder()` method. Instance of current database connection is available in `$connection` property.

/--php
class BooksMapper extends Nextras\Orm\Mapper\Mapper
{
	public function getRandomBooks_builder()
	{
		return $this->builder()->addOrderBy('RAND()');
	}

	public function getRandomBooks_query()
	{
		return $this->connection->query('SELECT * FROM tbl_books ORDER BY RAND()');
	}
}
\--

See [repository chapter | repository] to learn how to access methods from mapper layer.

[1]: https://github.com/nextras/orm


code:
- [Introduction | default]
- [Stack | stack]
- [Model | model]
- [Repository | repository]
- [Collection | collection]
- [Entity | entity]
- [Relationships | relationships]
- [Mapper | mapper]
- [Conventions | conventions]
- [Events | events]

---

- [Migration to 2.0 | migrate_2.0]


code:
Migration Guide for 2.0
#######################

General
=======

- Minimal PHP version was increased to 5.5.
- Orm requires Nextras Dbal 2.0.

Entity
======

- Each entity has to define primary property; use `primary` or `primary-proxy` modifier.
  /--php
  /**
   * @property int $id {primary}
   * @property int $followers {1:m BookFollowers::$book}
   * ...
   */
  class Book extends Nextras\Orm\Entity\Entity {}

  /**
   * @property int $id {primary-proxy}
   * @property Book $book {m:1 Book::$followers} {primary}
   * @property User $user {m:1 User::$followedBooks} {primary}
   * ...
   */
  class BookFollowers extends Nextras\Orm\Entity\Entity {}
  \--

- 1:1d relationship modifier was renamed to 1:1. The old 1:1 relationship support was removed entirely.
  /--
  // old
  {1:1d UserEntity::$meta primary}

  // new
  {1:1 UserEntity::$meta, isMain=true}
  \--

- New format for modifier syntax: each argument has to be separated by comma; values are assigned by `=`; values can be an array.
  /--
  // old
  {modifier value another key:value}
  {enum self::ONE_CONSTANT self::ANOTHER}

  // new
  {modifier value, another, key=value, arrayValue=[val1, val2]}
  {enum self::ONE_CONSTANT, self::ANOTHER}
  \--

- Relationship modifiers requires defined target property, if relationship is symetric (newly, relationship can be oneSided, see [Relationships | relationships] chapter). Old and deprecated relationship format was removed.
  /--
  // old
  {1:m UsersRepository}
  {1:m UsersRepository $posts}

  // new
  {1:m UserEntity::$posts}
  {1:m UserEntity::$posts}
  \--

- Changes were made to keys names and accepted values of Relationship modifiers.
  `order` -> `orderBy`
  `primary` -> `isMain`
  /--
  // old
  {1:m UserEntity::$posts order:id}
  {1:m UserEntity::$posts order:id,DESC}

  // new
  {1:m UserEntity::$posts, orderBy=id}
  {1:m UserEntity::$posts, orderBy=[id, DESC]}

  // old
  {m:m UserEntity::$posts primary}

  // new
  {m:m UserEntity::$posts, isMain=true}
  \--

- Modifiers no longer allow random and unused parameters.
  /--
  // old
  {primary foo bar}
  \--

- Modifiers now supports literal parsing, this may break some expresions that were previously parsed as string, now they are evaluated as in php.
  /--
  // old - resulted into string "123"
  {default 123}

  // new
  {default "123"}
  // or for int
  {default 123}
  \--

Repository
==========

- Each repository has to define static method `getEntityClassNames()` that returns accepted entity class names.
  /--php
  class BooksRepository extends Nextras\Orm\Repository\Repository
  {
       public static function getEntityClassNames()
       {
            return [Book::class];
       }
  }
  \--

- Repository no longer supports `@method` annotations that are not proxies for its mapper; you have to rewrite these methods manually.
  /--php
  // old

  /**
   * @method Book|null getByTitle($title)
   */
  class BooksRepository extends Nextras\Orm\Repository\Repository
  {
       // ...
  }

  // new

  class BooksRepository extends Nextras\Orm\Repository\Repository
  {
       // ...

       /**
        * @param  string $title
        * @return Book|null
        */
       public function getByTitle($title)
       {
            return $this->getBy(['title' => $title]);
       }
  }
  \--

- Recursive remove no longer works. The second parameter of `IRepository::remove()` method was renamed to `$withCascade`. The cascade is definied as a parameter in relationship modifier, the removal cascade is not added by default, only the perists one. See [Relationships | relationships] chapter.
  /--php
  // old
  $authorRepository->remove($author, true);

  // new
  /**
   * @property OneHasMany|Book[] $books {1:m Book::$author, cascade=[persist, remove]}
   */
  class Author extends Nextras\Orm\Entity\Entity {}
  $authorRepository->remove($author, true);
  \--

Mapper
=====

- Removed DbalMapper::createCollection() method. Override findAll() method to modify maximal set of entities.
- DbalMapper was refactored, if you are overriding relationship mappers, you have to do this by new `createRelationshipMapper()` and `getRelationshipMapper()` methods.


code:
Model
#####

Model is central manager for repositories. It holds references to them and manages their loading. Model requires repository loader. Orm comes with predefined loader for "Nette\DI":http://doc.nette.org/en/dependency-injection component. This loader uses dependency injection container for lazy loading of your repositories.

To define model repositories use php doc `@property-read` annotations.

/--php
namespace MyApp;

/**
 * @property-read PostsRepository $posts
 * @property-read UsersRepository $users
 * @property-read TagsRepository $tags
 */
class Orm extends \Nextras\Orm\Model\Model
{
}
\--

Then add Orm extension into your application `config.neon`:

/--neon
extensions:
	orm: Nextras\Orm\Bridges\NetteDI\OrmExtension

orm:
	model: MyApp\Orm
	metadataParserFactory: MyApp\MyMetadataParserFactory
\--

The key `model` accepts class name of your project's model. Optionally, you may define your metadata parser factory, in which you will be able to customize metadata parsing (add support to new modifiers, etc.). You can easily inject Orm class into your presenters / services / classes and use property access to get needed repositories:

/--php
namespace MyApp;

class MyService
{
	/** @var Orm */
	private $orm;

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

	public function doSomething($postId)
	{
		$post = $this->orm->posts->getById($postId);
		// ...
	}
}
\--

Persistence & removal
---------------------

Model itlsef ensures the cascade peristance or removal behavior. However, you can also use its method to do non-cascade action. Call `persist()` or `remove()` method to process the passed entity in proper repository. At the end of your work, you should confirm your changes by flushing them. Flushing is internally implemented as a commiting of a transaction; transactions are automatically opened with your first persistence or removal call. Not only newly created entities, but also **already persisted entities must be persisted again to promote its changes to the storage**. This bahavior differs to Doctrine, which automatically saves all changes in attached entities.

/--php
$user = $model->users->getById(1);
$user->isEmailSubscribed = false;

$model->persist($user);
$model->flush();
// or
$model->persistAndFlush($user);
\--

-----

Model with simple loader
========================

If you do not use Nette\DI, you can use predefined SimpleRepositoryLoader. This loader requires already instantiated array of repositories. For creating the stack easily, you can use SimpleLoaderFactory. You have to create instances of repositories and mappers on your own.

/--php
$cache = new Nette\Caching\Cache(...);
$connection = new Nextras\Dbal\Connection(...);
$metadataParserFactory = new Nextras\Orm\Entity\Reflection\MetadataParserFactory();

$simpleModelFactory = new SimpleModelFactory([
	'posts' => new MyApp\PostsRepository(new MyApp\PostsMapper($connection, $cache)),
	'users' => new MyApp\UsersRepository(new MyApp\UsersMapper($connection, $cache)),
	'tags' => new MyApp\TagsRepository(new MyApp\TagsMapper($connection, $cache)),
], $cache, $metadataParserFactory);

$model = $simpleModelFactory->create();
\--

Of course, you can create your own repository loader by implementing `Nextras\Orm\Model\IRepositoryLoader` interface. Metadata parser factory optional dependency.


code:
Relationships
=============

Orm provides very efficient way to work with entity relationships. Orm recognizes 5 types of relationships:

- **1:m** - one has many: *author has many books*
- **m:1** - many has one: *book has one author*
- **m:n** - many has many: *book has many tags, tag is associated with many books*
- **1:1** - one has one: *user has one settings*, the reference for related entity is stored only on the side that is marked as main.

Use a relationship modifier to define relationship property. Modifiers require to define a target entity, some modifiers need to be defined on the both sides, then the reverse property definition is compulsory. If you want to define only one-sided relationship, use `oneSided=true` parameter. Other parameters are optional: ordering, setting a cascade, or making the current side primary (persisting is driven by the primary side). At least one side of `m:n` or `1:1d` has to be defined as the primary. Relationships do not support getters and setters as other entity properties.

/--code php
{1:1 EntityName::$reversePropertyName}
{m:1 EntityName::$reversePropertyName}
{1:m EntityName::$reversePropertyName, orderBy=property}
{m:n EntityName::$reversePropertyName, isMain=true, orderBy=[property, DESC]}
\--

Cascade
-------

All relationships can have defined cascade behaviour. Cascade behavior defined if peristance or removal of entity should affect connected entities. By default, all relationship have defined cascade for persistence. Cascade is defined as an array of keywords: persist and remove are currently supported options. Cascade works for every type of relationship.

/--
// persist cascade is the default
{relModifier EntityName::$reversePropertyName} // equals to
{relModifier EntityName::$reversePropertyName, cascade=[persist]}

// adding remove cascade; you have to redefine persist cascade
{relModifier EntityName::$reversePropertyName, cascade=[persist, remove]}

// to disable persist cascade, provide empty cascade definition
{relModifier EntityName::$reversePropertyName, cascade=[]}
\--

By default, the persist and remove methods process entity with its cascade. You can turn off this behavior by the second method argument:

/--php
$usersRepository->persist($user, false);
$usersRepository->remove($user, false);
\--

1:M / M:1 - Bidirectional
-------------------------

/--code php
use Nextras\Orm\Relationships\OneHasMany;

/**
 * @property int                $id               {primary}
 * @property OneHasMany|Book[]  $books            {1:m Book::$author}
 * @property OneHasMany|Book[]  $translatedBooks  {1:m Book::$translator}
 */
class Author extends Nextras\Orm\Entity\Entity
{}

/**
 * @property int     $id          {primary}
 * @property Author  $author      {m:1 Author::$books}
 * @property Author  $translator  {m:1 Author::$translatedBooks}
 */
class Book extends Nextras\Orm\Entity\Entity
{}
\--

1:M - One-sided
---------------

/--code php
/**
 * @property int     $id          {primary}
 * @property Author  $author      {m:1 Author, oneSided=true}
 * @property Author  $translator  {m:1 Author, oneSided=true}
 */
class Book extends Nextras\Orm\Entity\Entity
{}
\--

1:M / M:1 - Self-referencing
----------------------------

/--code php
use Nextras\Orm\Relationships\OneHasMany;

/**
 * @property int                    $id          {primary}
 * @property Category               $parent      {m:1 Category::$categories}
 * @property OneHasMany|Category[]  $categories  {1:m Category::$parent}
 */
class Category extends Nextras\Orm\Entity\Entity
{}
\--

M:M - Bidirectional
-------------------

/--code php
use Nextras\Orm\Relationships\ManyHasMany;

/**
 * @property int                $id    {primary}
 * @property ManyHasMany|Tag[]  $tags  {m:n Tag::$books, isMain=true}
 */
class Book extends Nextras\Orm\Entity\Entity
{}

/**
 * @property int                 $id     {primary}
 * @property ManyHasMany|Book[]  $books  {m:n Book::$tags}
 */
class Tag extends Nextras\Orm\Entity\Entity
{}
\--

M:M - One-sided
---------------

Only the non-primary side is optional.

/--code php
use Nextras\Orm\Relationships\ManyHasMany;

/**
 * @property int                $id    {primary}
 * @property ManyHasMany|Tag[]  $tags  {m:n Tag, isMain=true, oneSided=true}
 */
class Book extends Nextras\Orm\Entity\Entity
{}
\--

M:M - Self-referencing
----------------------

/--code php
use Nextras\Orm\Relationships\ManyHasMany;

/**
 * @property int                 $id             {primary}
 * @property ManyHasMany|User[]  $myFriends      {m:m User::$friendsWithMe}
 * @property ManyHasMany|User[]  $friendsWithMe  {m:m User::$myFriends}
 */
class User extends Nextras\Orm\Entity\Entity
{}
\--

1:1 - Bidirectional
-------------------

Reference will be stored in `book.ean_id`.

/--code php
/**
 * @property int      $id   {primary}
 * @property EanCode  $ean  {1:1 Ean::$book, isMain=true}
 */
class Book extends Nextras\Orm\Entity\Entity
{}

/**
 * @property int   $id    {primary}
 * @property Book  $book  {1:1 Book::$ean}
 */
class Tag extends Nextras\Orm\Entity\Entity
{}
\--

1:1 - One-sided
---------------

Only the non-primary side is optional. Reference will be stored in `book.ean_id`.

/--code php
/**
 * @property int      $id   {primary}
 * @property EanCode  $ean  {1:1 Ean, isMain=true, oneSided=true}
 */
class Book extends Nextras\Orm\Entity\Entity
{}
\--

1:1 - Self-referencing
----------------------

Reference will be stored in `book.next_volume_id`.

/--code php
/**
 * @property int        $id              {primary}
 * @property Book|null  $nextVolume      {1:1 Book::$previousVolume, isMain=true}
 * @property Book|null  $previousVolume  {1:1 Book::$nextVolume}
 */
class Book extends Nextras\Orm\Entity\Entity
{}
\--

------------

Relationship interfaces
-----------------------

The example above introduces classes which weren't mentioned before: `OneHasMany` and `ManyHasMany`. Instances of these classes are injected into the property and provide some cool features. The main responsibility is the implementation of `\Traversable` interface. You can easily iterate over the property to get the entities in the relationship.

/--php
foreach ($author->books as $book) {
	$book instanceof Book; // true
}
\--

Also, you can use very clever interface to add, remove, and set entities in relationship. Sometimes, it is useful to work with the relationship collection as with the `ICollection`. Just use `get()` method to get it.

/--php
use Nextras\Orm\Collection\ICollection;

$author->books->add($book);
$author->books->remove($book);
$author->books->set([$book]);
$author->books->get() instanceof ICollection; // true

$book->tags->add($tag);
$book->tags->remove($tag);
$book->tags->set([$tag]);
$book->tags->get() instanceof ICollection; // true
\--

These methods accept both entity instances and primary values. If you pass primary value, Orm will load the proper entity automatically.

/--php
$book->author = 1;
$book->author->id === 1; // true
\--


code:
Repository
##########

Repository provides interface for entities retrieving, persisting and removing.

/--div .[advice]
In Orm, we use coding standard which assumes, that
- `get*` methods return IEntity instance or null,
- `find*` methods return ICollection instance.
\--

Retrieving
==========

Repository provides `findAll()` method, which returns `Nextras\Orm\Collection\ICollection` instance with all entities in storage. You can add filtering conditions, sort and fetch entities from the collection. Read more about [Collection in its chapter | collection].

Repository has to define static method `getEntityClassNames()` that returns array of entity names that the repsitory produce. Repository itself can contain user defined methods:

/--php
final class BooksRepository extends Repository
{
	static function getEntityClassNames()
	{
		return [Book::class];
	}

	/**
	 * @return ICollection|Book[]
	 */
	public function findLatest()
	{
		return $this->findAll()->orderBy('id', ICollection::DESC)->limitBy(3);
	}

	/**
	 * @return ICollection|Book[]
	 */
	public function findByTags($name)
	{
		return $this->findBy(['this->tags->name' => $name]);
	}
}
\--

Sometimes, it is needed to write pure SQL query. SQL queries can be written only in mapper layer. You can easily tell repository to proxy these methods by writing php doc `@method` annotation:

/--php
/**
 * @method ICollection|Book[] findBooksWithEvenId()
 */
final class BooksRepository extends Repository
{
	// ...
}

final class BooksMapper extends Mapper
{
	public function findBooksWithEvenId()
	{
		return $this->builder()->where('id % 2 = 0');
	}
}
\--

.[note]
In the example above you can see that the mapper layer returns `Nextras\Dbal\QueryBuilder\QueryBuilder` object, but annotation says that repository will return `ICollection` object. If mapper does not return ICollection, Entity or null value, repository automatically calls `IMapper::toCollection()` method. You can return only things which your mapper can automatically convert to allowed types.

---------

Identity map
============

Repository uses Identity Map pattern. Therefore only one instance of Entity can exist in your runtime. Selecting the same entity by another query will still return the same entity, even when entity changes were not persisted.

/--php
// in this example title property is unique

$book1 = $orm->books->getById(1);
$book2 = $orm->books->findBy(['title' => $book1->title])->fetch();

$book1 === $book2; // true
\--

---------

Persisting
==========

Each entity can be persisted. It does not matter if you are creating or updating entity, for each case you use `IRepository::persist()` method. By default, repository will persist all other connected entities. (And also will take care of needed persistence order.) But you can persist only specific entity by providing false as the second argument of persist method.

Persistence is run in transaction. Calling persist method automatically starts a transaction (if not started earlier). The transaction commit is done by `IRepository::flush()` method. You can persist and flush changes at once by using `IRepository::persistAndFlush()` method. Persisting automatically attaches entity to repository, if it has not been attached earlier.

/--php
$author = new Author();
$author->name = 'Jon Snow';
$author->born = 'yesterday';
$author->mail = 'snow@wall.st';

// let Orm know about entity
// nothing has been inserted into database yet
$orm->authors->attach($author);

$book = new Book();
$book->title = 'My Life on The Wall';
$book->author = $author;
$book->publisher = 1; // id of stored publisher in db

// stores new book and author entity into database
// queries are run in transaction and commited
$orm->books->persistAndFlush($book);
\--

-------------

Removing
========

Use `IRepository::remove()` method to delete entities from database.

If entity has property with OneHasMany relationship type and the property on other side is not nullable, removing this entity would cause throwing an error, because the property on the other side cannot be nulled. E.g.: you cannot remove author with books, because book has compulsory author property. The solution is:
- to set new author for the books,
- remove books first,
- enable recursive removal.

Recursive removal is not enabled by default; you can enable it by passing optional second boolean parameter.

/--php
$author = $orm->authors->getById(1);
// $author->books collection is not empty

// recursively removes entities
// which cannot stay without $author entity
$orm->authors->remove($author, true);
\--

Removing entities is run in transaction as well as persisting. At the end you have to call `IRepository::flush()` method or use `IRepository::removeAndFlush()` method.


code:
Stack
#####

Orm is clearly designed to abstract entities from database implementation, therefore it separates your model into three basic layers:
- **Entities**
 [Entities | entity] are data crates, hold your data, validate values and provide some API for entities connection, relationships and traversal.
- **Repositories**
 [Repositories | repository] form a layer which takes care about your entities. Repository requires a mapper as a dependency. Repository does not know anything about your storage (e.g. database), only manages entities and provides API for entities getting, filtering, storing and removing.
- **Mappers**
 [Mappers | mapper] are the backend of the whole Orm. Mapper provides interaction with database layer. Orm comes with mapper layer which uses "Nextras\Dbal":[1] library.

Everything is connected in central Model, which manages all repositories.

[1]: https://github.com/nextras/dbal


code:
Nextras Orm
===========

[![Build Status](https://travis-ci.org/nextras/orm.svg?branch=master)](https://travis-ci.org/nextras/orm)
[![Downloads this Month](https://img.shields.io/packagist/dm/nextras/orm.svg?style=flat)](https://packagist.org/packages/nextras/orm)
[![Stable version](http://img.shields.io/packagist/v/nextras/orm.svg?style=flat)](https://packagist.org/packages/nextras/orm)
[![Code coverage](https://img.shields.io/coveralls/nextras/orm.svg?style=flat)](https://coveralls.io/r/nextras/orm)

### Installation

Use composer:

```bash
$ composer require nextras/orm
```

### Docs & sources

- [Documentation](https://nextras.org/orm/docs)
- [Api Documentation](https://codedoc.pub/nextras/orm)
- [Talk at Posledni sobota](https://www.youtube.com/watch?v=6MnONy6DTLs) (czech only), [repo](https://github.com/hrach/orm-demo)

### Tips

- [Nextras ORM PhpStorm Plug-in](https://github.com/nextras/orm-intellij)
- [Extensions for ORM](https://github.com/Mikulas/nextras-ormext)

### License

MIT. See full [license](license.md).


code:
<?php

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

namespace Nextras\Orm\Bridges\NetteDI;

use Nette\DI\Container;
use Nextras\Orm\Entity\IEntity;
use Nextras\Orm\Repository\IDependencyProvider;

class DependencyProvider implements IDependencyProvider
{
	/** @var Container */
	private $container;

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

	public function injectDependencies(IEntity $entity)
	{
		$this->container->callInjects($entity);
		return $entity;
	}
}


code:
<?php

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

namespace Nextras\Orm\Bridges\NetteDI;

use Nette\Caching\Cache;
use Nette\DI\CompilerExtension;
use Nette\DI\ContainerBuilder;
use Nette\PhpGenerator;
use Nette\Reflection\AnnotationsParser;
use Nette\Reflection\ClassType;
use Nextras\Orm\Entity\Reflection\MetadataParserFactory;
use Nextras\Orm\InvalidStateException;
use Nextras\Orm\Model\MetadataStorage;
use Nextras\Orm\Model\Model;
use Nextras\Orm\RuntimeException;

class OrmExtension extends CompilerExtension
{
	public function loadConfiguration()
	{
		$configDefaults = [
			'metadataParserFactory' => MetadataParserFactory::class,
		];

		$config = $this->getConfig($configDefaults);
		if (!isset($config['model'])) {
			throw new InvalidStateException('Model is not defined.');
		}

		$repositories = $this->getRepositoryList($config['model']);
		$repositoriesConfig = Model::getConfiguration($repositories);

		$this->setupCache();
		$this->setupDependencyProvider();
		$this->setupMetadataParserFactory($config['metadataParserFactory']);
		$this->setupRepositoryLoader($repositories);
		$this->setupMetadataStorage($repositoriesConfig);
		$this->setupRepositoriesAndMappers($repositories);
		$this->setupModel($config['model'], $repositoriesConfig);
	}

	protected function getRepositoryList($modelClass)
	{
		$modelReflection = new ClassType($modelClass);

		$builder = $this->getContainerBuilder();
		$builder->addDependency($modelReflection->getFileName());

		$repositories = [];
		foreach ($modelReflection->getAnnotations() as $key => $annotations) {
			if ($key !== 'property-read') {
				continue;
			}

			foreach ($annotations as $annotation) {
				list($class, $name) = preg_split('#\s+#', $annotation);
				$class = AnnotationsParser::expandClassName($class, $modelReflection);
				if (!class_exists($class)) {
					throw new RuntimeException("Repository '{$class}' does not exist.");
				}

				$repositories[ltrim($name, '$')] = $class;
			}
		}

		return $repositories;
	}

	protected function setupCache()
	{
		$builder = $this->getContainerBuilder();
		$providerName = $this->prefix('cache');
		if (!$builder->hasDefinition($providerName)) {
			$builder->addDefinition($providerName)
				->setClass(Cache::class)
				->setArguments([
					'namespace' => $this->name,
				])
				->setAutowired(false);
		}
	}

	protected function setupDependencyProvider()
	{
		$builder = $this->getContainerBuilder();
		$providerName = $this->prefix('dependencyProvider');
		if (!$builder->hasDefinition($providerName)) {
			$builder->addDefinition($providerName)
				->setClass(DependencyProvider::class);
		}
	}

	protected function setupMetadataParserFactory($class)
	{
		$builder = $this->getContainerBuilder();
		$builder->addDefinition($this->prefix('metadataParserFactory'))
			->setClass($class);
	}

	protected function setupRepositoryLoader(array $repositories)
	{
		$map = [];
		foreach ($repositories as $name => $className) {
			$map[$className] = $this->prefix('repositories.' . $name);
		}

		$builder = $this->getContainerBuilder();
		$builder->addDefinition($this->prefix('repositoryLoader'))
			->setClass(RepositoryLoader::class)
			->setArguments([
				'repositoryNamesMap' => $map,
			]);
	}

	protected function setupMetadataStorage(array $repositoryConfig)
	{
		$builder = $this->getContainerBuilder();
		$builder->addDefinition($this->prefix('metadataStorage'))
			->setClass(MetadataStorage::class)
			->setArguments([
				'entityClassesMap' => $repositoryConfig[2],
				'cache' => '@' . $this->prefix('cache'),
				'metadataParserFactory' => '@' . $this->prefix('metadataParserFactory'),
				'repositoryLoader' => '@' . $this->prefix('repositoryLoader'),
			]);
	}

	protected function setupRepositoriesAndMappers($repositories)
	{
		$builder = $this->getContainerBuilder();

		foreach ($repositories as $repositoryName => $repositoryClass) {
			$mapperName = $this->createMapperService($repositoryName, $repositoryClass, $builder);
			$this->createRepositoryService($repositoryName, $repositoryClass, $builder, $mapperName);
		}
	}

	protected function createMapperService($repositoryName, $repositoryClass, ContainerBuilder $builder)
	{
		$mapperName = $this->prefix('mappers.' . $repositoryName);
		if (!$builder->hasDefinition($mapperName)) {
			$mapperClass = str_replace('Repository', 'Mapper', $repositoryClass);
			if (!class_exists($mapperClass)) {
				throw new InvalidStateException("Unknown mapper for '{$repositoryName}' repository.");
			}

			$builder->addDefinition($mapperName)
				->setClass($mapperClass)
				->setArguments([
					'cache' => '@' . $this->prefix('cache'),
				]);
		}

		return $mapperName;
	}

	protected function createRepositoryService($repositoryName, $repositoryClass, ContainerBuilder $builder, $mapperName)
	{
		$repositoryName = $this->prefix('repositories.' . $repositoryName);
		if (!$builder->hasDefinition($repositoryName)) {
			$builder->addDefinition($repositoryName)
				->setClass($repositoryClass)
				->setArguments([
					'@' . $mapperName,
					'@' . $this->prefix('dependencyProvider'),
				])
				->addSetup('setModel', ['@' . $this->prefix('model')]);
		}
	}

	protected function setupModel($modelClass, $repositoriesConfig)
	{
		$builder = $this->getContainerBuilder();

		$builder->addDefinition($this->prefix('model'))
			->setClass($modelClass)
			->setArguments([
				'configuration' => $repositoriesConfig,
				'repositoryLoader' => '@' . $this->prefix('repositoryLoader'),
				'metadataStorage' => '@' . $this->prefix('metadataStorage'),
			]);
	}
}


code:
<?php

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

namespace Nextras\Orm\Bridges\NetteDI;

use Nette\DI\ContainerBuilder;
use Nextras\Orm\TestHelper\EntityCreator;
use Nextras\Orm\TestHelper\TestMapper;

class OrmTestsExtension extends OrmExtension
{
	/** @var bool */
	private $testingMappers = true;

	public function loadConfiguration()
	{
		$config = $this->getConfig(['testingMappers' => true]);
		$this->testingMappers = $config['testingMappers'];
		parent::loadConfiguration();
		$this->setupEntityCreator();
	}

	protected function setupEntityCreator()
	{
		$builder = $this->getContainerBuilder();
		$builder->addDefinition($this->prefix('testing.entityCreator'))
			->setClass(EntityCreator::class)
			->setArguments(['@' . $this->prefix('model')]);
	}

	protected function createMapperService($repositoryName, $repositoryClass, ContainerBuilder $builder)
	{
		if (!$this->testingMappers) {
			return parent::createMapperService($repositoryName, $repositoryClass, $builder);
		}

		$testMapperName = $this->prefix('mappers.testing.' . $repositoryName);
		if (!$builder->hasDefinition($testMapperName)) {
			$mapperClass = TestMapper::class;
			$builder->addDefinition($testMapperName)->setClass($mapperClass);
		}

		return $testMapperName;
	}
}


code:
<?php

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

namespace Nextras\Orm\Bridges\NetteDI;

use Nette\DI\Container;
use Nextras\Orm\Model\IRepositoryLoader;

class RepositoryLoader implements IRepositoryLoader
{
	/** @var Container */
	private $container;

	/** @var array */
	private $repositoryNamesMap;

	public function __construct(Container $container, array $repositoryNamesMap)
	{
		$this->container = $container;
		$this->repositoryNamesMap = $repositoryNamesMap;
	}

	public function hasRepository($className)
	{
		return isset($this->repositoryNamesMap[$className]);
	}

	public function getRepository($className)
	{
		return $this->container->getService($this->repositoryNamesMap[$className]);
	}

	public function isCreated($className)
	{
		return $this->container->isCreated($this->repositoryNamesMap[$className]);
	}
}


code:
<?php

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

namespace Nextras\Orm\Collection;

use Iterator;
use Nextras\Orm\Collection\Helpers\ArrayCollectionHelper;
use Nextras\Orm\Collection\Helpers\FetchPairsHelper;
use Nextras\Orm\Entity\IEntity;
use Nextras\Orm\Mapper\IRelationshipMapper;
use Nextras\Orm\MemberAccessException;
use Nextras\Orm\Repository\IRepository;

class ArrayCollection implements ICollection
{
	/** @var array */
	protected $data;

	/** @var IRelationshipMapper */
	protected $relationshipMapper;

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

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

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

	/** @var ArrayCollectionHelper */
	protected $helper;

	/** @var array */
	protected $collectionFilter = [];

	/** @var array */
	protected $collectionSorter = [];

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

	public function __construct(array $data, IRepository $repository)
	{
		$this->data = $data;
		$this->repository = $repository;
	}

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

	public function findBy(array $where)
	{
		$collection = clone $this;
		foreach ($where as $column => $value) {
			$collection->collectionFilter[] = $this->getHelper()->createFilter($column, $value);
		}
		return $collection;
	}

	public function orderBy($column, $direction = self::ASC)
	{
		$collection = clone $this;
		if (is_array($column)) {
			foreach ($column as $col => $direction) {
				$collection->collectionSorter[] = [$col, $direction];
			}
		} else {
			$collection->collectionSorter[] = [$column, $direction];
		}
		return $collection;
	}

	public function resetOrderBy()
	{
		$collection = clone $this;
		$collection->collectionSorter = [];
		return $collection;
	}

	public function limitBy($limit, $offset = null)
	{
		$collection = clone $this;
		$collection->collectionLimit = [$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 ($parent && $this->relationshipMapper) {
			$collection = clone $this;
			$collection->relationshipMapper = null;
			$collection->relationshipParent = null;
			return $this->relationshipMapper->getIterator($parent, $collection);

		} else {
			$this->processData();
			return new EntityIterator(array_values($this->data));
		}
	}

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

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

	public function getEntityCount(IEntity $parent = null)
	{
		return count($this->getEntityIterator($parent));
	}

	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->fetchIterator = null;
	}

	protected function processData()
	{
		if ($this->collectionFilter || $this->collectionSorter || $this->collectionLimit) {
			$data = $this->data;
			foreach ($this->collectionFilter as $filter) {
				$data = array_filter($data, $filter);
			}

			if ($this->collectionSorter) {
				$sorter = $this->getHelper()->createSorter($this->collectionSorter);
				usort($data, $sorter);
			}

			if ($this->collectionLimit) {
				$data = array_slice($data, $this->collectionLimit[1], $this->collectionLimit[0]);
			}

			$this->collectionFilter = [];
			$this->collectionSorter = [];
			$this->collectionLimit = null;
			$this->data = $data;
		}
	}

	protected function getHelper()
	{
		if ($this->helper === null) {
			$this->helper = new ArrayCollectionHelper($this->repository->getModel(), $this->repository->getMapper());
		}

		return $this->helper;
	}
}


code:
<?php

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

namespace Nextras\Orm\Collection;

use Nextras\Orm\Mapper\Dbal\DbalCollection;

class Collection extends DbalCollection
{
}


code:
<?php

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

namespace Nextras\Orm\Collection;

use Nextras\Orm\Entity\IEntity;
use Nextras\Orm\Mapper\IRelationshipMapper;

final class EmptyCollection implements ICollection
{
	/** @var IRelationshipMapper */
	private $relationshipMapper;

	public function getBy(array $where)
	{
		return null;
	}

	public function findBy(array $where)
	{
		return clone $this;
	}

	public function orderBy($column, $direction = self::ASC)
	{
		return clone $this;
	}

	public function resetOrderBy()
	{
		return clone $this;
	}

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

	public function fetch()
	{
		return null;
	}

	public function fetchAll()
	{
		return [];
	}

	public function fetchPairs($key = null, $value = null)
	{
		return [];
	}

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

	public function getIterator()
	{
		return new \EmptyIterator();
	}

	public function getEntityIterator(IEntity $parent = null)
	{
		return new \EmptyIterator();
	}

	public function getEntityCount(IEntity $parent = null)
	{
		return 0;
	}

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

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

	public function countStored()
	{
		return 0;
	}

	public function count()
	{
		return 0;
	}
}


code:
<?php

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

namespace Nextras\Orm\Collection;

class EntityContainer implements IEntityContainer
{
	/** @var array */
	private $data;

	/** @var array */
	private $preloadCache;

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

	public function getEntity($key)
	{
		if (!isset($this->data[$key])) {
			return null;
		}

		$current = $this->data[$key];
		$current->setPreloadContainer($this);
		return $current;
	}

	public function getPreloadValues($property)
	{
		if (isset($this->preloadCache[$property])) {
			return $this->preloadCache[$property];
		}

		$values = [];
		foreach ($this->data as $entity) {
			$values[] = $entity->getRawValue($property);
		}
		return $this->preloadCache[$property] = $values;
	}
}


code:
<?php

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

namespace Nextras\Orm\Collection;

use Nextras\Orm\Entity\IEntity;

class EntityIterator implements IEntityIterator
{
	/** @var int */
	private $position = 0;

	/** @var array */
	private $data;

	/** @var array */
	private $iteratable;

	/** @var bool */
	private $hasSubarray = false;

	/** @var array */
	private $preloadCache;

	public function __construct(array $data)
	{
		$this->data = $data;
		$this->setDataIndex(null);
	}

	public function setDataIndex($index)
	{
		if (isset($index)) {
			if (!isset($this->data[$index])) {
				$this->data[$index] = [];
			}
			$this->iteratable = & $this->data[$index];
			$this->hasSubarray = true;
		} else {
			$this->iteratable = & $this->data;
			$this->hasSubarray = false;
		}

		$this->rewind();
	}

	public function next()
	{
		++$this->position;
	}

	/**
	 * @return IEntity
	 */
	public function current()
	{
		if (!isset($this->iteratable[$this->position])) {
			return null;
		}

		$current = $this->iteratable[$this->position];
		$current->setPreloadContainer($this);
		return $current;
	}

	public function key()
	{
		return $this->position;
	}

	public function valid()
	{
		return isset($this->iteratable[$this->position]);
	}

	public function rewind()
	{
		$this->position = 0;
	}

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

	public function getPreloadValues($property)
	{
		if (isset($this->preloadCache[$property])) {
			return $this->preloadCache[$property];
		}

		$values = [];

		if ($this->hasSubarray) {
			foreach ($this->data as $block) {
				foreach ($block as $entity) {
					$values[] = $entity->getRawValue($property);
				}
			}
		} else {
			foreach ($this->data as $entity) {
				$values[] = $entity->getRawValue($property);
			}
		}

		return $this->preloadCache[$property] = $values;
	}
}


code:
<?php

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

namespace Nextras\Orm\Collection\Helpers;

use Closure;
use Nextras\Orm\Collection\ICollection;
use Nextras\Orm\Entity\IEntity;
use Nextras\Orm\Entity\Reflection\EntityMetadata;
use Nextras\Orm\InvalidArgumentException;
use Nextras\Orm\InvalidStateException;
use Nextras\Orm\Mapper\IMapper;
use Nextras\Orm\Model\IModel;
use Nextras\Orm\Model\MetadataStorage;
use Nextras\Orm\Relationships\IRelationshipCollection;

class ArrayCollectionHelper
{
	/** @var IModel */
	private $model;

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

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

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

	/**
	 * @param  string $condition
	 * @param  mixed  $value
	 * @return Closure
	 */
	public function createFilter($condition, $value)
	{
		list($chain, $operator, $sourceEntity) = ConditionParserHelper::parseCondition($condition);
		$sourceEntityMeta = $this->metadataStorage->get($sourceEntity ?: $this->mapper->getRepository()->getEntityClassNames()[0]);

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

		if ($operator === ConditionParserHelper::OPERATOR_EQUAL) {
			if (is_array($value)) {
				$predicate = function ($property, $value) {
					return in_array($property, $value, true);
				};
			} else {
				$predicate = function ($property, $value) {
					return $property === $value;
				};
			}
		} elseif ($operator === ConditionParserHelper::OPERATOR_NOT_EQUAL) {
			if (is_array($value)) {
				$predicate = function ($property, $value) {
					return !in_array($property, $value, true);
				};
			} else {
				$predicate = function ($property, $value) {
					return $property !== $value;
				};
			}
		} elseif ($operator === ConditionParserHelper::OPERATOR_GREATER) {
			$predicate = function ($property, $value) {
				return $property > $value;
			};
		} elseif ($operator === ConditionParserHelper::OPERATOR_EQUAL_OR_GREATER) {
			$predicate = function ($property, $value) {
				return $property >= $value;
			};
		} elseif ($operator === ConditionParserHelper::OPERATOR_SMALLER) {
			$predicate = function ($property, $value) {
				return $property < $value;
			};
		} elseif ($operator === ConditionParserHelper::OPERATOR_EQUAL_OR_SMALLER) {
			$predicate = function ($property, $value) {
				return $property <= $value;
			};
		} else {
			throw new InvalidArgumentException();
		}

		return $this->createFilterEvaluator($chain, $predicate, $sourceEntityMeta, $value);
	}

	protected function createFilterEvaluator($chainSource, Closure $predicate, EntityMetadata $sourceEntityMetaSource, $targetValue)
	{
		$evaluator = function (
			$element,
			$chain = null,
			EntityMetadata $sourceEntityMeta = null
		) use (
			& $evaluator,
			$predicate,
			$chainSource,
			$sourceEntityMetaSource,
			$targetValue
		) {
			if (!$chain) {
				$sourceEntityMeta = $sourceEntityMetaSource;
				$chain = $chainSource;
			}

			$column = array_shift($chain);
			$propertyMeta = $sourceEntityMeta->getProperty($column); // check if property exists
			$value = $element->hasValue($column) ? $element->getValue($column) : null;

			if (!$chain) {
				if ($column === 'id' && count($sourceEntityMeta->getPrimaryKey()) > 1 && !isset($targetValue[0][0])) {
					$targetValue = [$targetValue];
				}
				return $predicate($this->simplifyValue($value), $targetValue);
			}

			$targetEntityMeta = $this->metadataStorage->get($propertyMeta->relationship->entity);
			if ($value === null) {
				return false;

			} elseif ($value instanceof IRelationshipCollection) {
				foreach ($value as $node) {
					if ($evaluator($node, $chain, $targetEntityMeta)) {
						return true;
					}
				}

				return false;
			} else {
				return $evaluator($value, $chain, $targetEntityMeta);
			}
		};

		return $evaluator;
	}

	/**
	 * @param  string $condition
	 * @param  string $direction
	 * @return Closure
	 */
	public function createSorter(array $conditions)
	{
		$columns = [];
		foreach ($conditions as $pair) {
			list($column, , $sourceEntity) = ConditionParserHelper::parseCondition($pair[0]);
			$sourceEntityMeta = $this->metadataStorage->get($sourceEntity ?: $this->mapper->getRepository()->getEntityClassNames()[0]);
			$columns[] = [$column, $pair[1], $sourceEntityMeta];
		}

		$getter = function ($element, $chain, EntityMetadata $sourceEntityMeta) use (& $getter) {
			$column = array_shift($chain);
			$propertyMeta = $sourceEntityMeta->getProperty($column); // check if property exists
			$value = $element->hasValue($column) ? $element->getValue($column) : null;

			if ($value instanceof IRelationshipCollection) {
				throw new InvalidStateException('You can not sort by hasMany relationship.');
			}

			if (!$chain) {
				return $value;
			} else {
				$targetEntityMeta = $this->metadataStorage->get($propertyMeta->relationship->entity);
				return $getter($value, $chain, $targetEntityMeta);
			}
		};

		return function ($a, $b) use ($getter, $columns) {
			foreach ($columns as $pair) {
				$_a = $this->simplifyValue($getter($a, $pair[0], $pair[2]));
				$_b = $this->simplifyValue($getter($b, $pair[0], $pair[2]));
				$direction = $pair[1] === ICollection::ASC ? 1 : -1;

				if (is_int($_a) || is_float($_a)) {
					if ($_a < $_b) {
						return $direction * -1;
					} elseif ($_a > $_b) {
						return $direction;
					}
				} else {
					$res = strcmp((string) $_a, (string) $_b);
					if ($res < 0) {
						return $direction * -1;
					} elseif ($res > 0) {
						return $direction;
					}
				}
			}

			return 0;
		};
	}

	private function simplifyValue($value)
	{
		if ($value instanceof IEntity) {
			return $value->hasValue('id') ? $value->getValue('id') : null;

		} elseif ($value instanceof \DateTime) {
			return $value->format('%U.%u');
		}

		return $value;
	}
}


code:
<?php

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

namespace Nextras\Orm\Collection\Helpers;

use Nextras\Orm\InvalidArgumentException;

class ConditionParserHelper
{
	/** @const operators */
	const OPERATOR_EQUAL = '=';
	const OPERATOR_NOT_EQUAL = '!=';
	const OPERATOR_GREATER = '>';
	const OPERATOR_EQUAL_OR_GREATER = '>=';
	const OPERATOR_SMALLER = '<';
	const OPERATOR_EQUAL_OR_SMALLER = '<=';

	public static function parseCondition($condition)
	{
		if (!preg_match('#^([\w\\\]+(?:->\w+)*)(!|!=|<=|>=|=|>|<)?$#', $condition, $matches)) {
			throw new InvalidArgumentException('Unsupported condition format.');
		}

		$source = null;
		$tokens = explode('->', $matches[1]);
		if (count($tokens) > 1) {
			$source = array_shift($tokens);
			$source = $source === 'this' ? null : $source;
		}

		return [
			$tokens,
			isset($matches[2]) ? ($matches[2] === '!' ? '!=' : $matches[2]) : '=',
			$source,
		];
	}
}


code:
<?php

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

namespace Nextras\Orm\Collection\Helpers;

use Nextras\Orm\InvalidArgumentException;
use Traversable;

class FetchPairsHelper
{
	public static function process(Traversable $collection, $key = null, $value = null)
	{
		$return = [];
		$rows = iterator_to_array($collection);

		if ($key === null && $value === null) {
			throw new InvalidArgumentException('FetchPairsHelper requires defined key or value.');
		}

		if ($key === null) {
			foreach ($rows as $row) {
				$return[] = $row->{$value};
			}

		} elseif ($value === null) {
			foreach ($rows as $row) {
				$return[is_object($row->{$key}) ? (string) $row->{$key} : $row->{$key}] = $row;
			}

		} else {
			foreach ($rows as $row) {
				$return[is_object($row->{$key}) ? (string) $row->{$key} : $row->{$key}] = $row->{$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\Collection;

use Countable;
use IteratorAggregate;
use Nextras\Orm\Entity\IEntity;
use Nextras\Orm\Mapper\IRelationshipMapper;
use Traversable;

interface ICollection extends IteratorAggregate, Countable
{
	/** @const asc order */
	const ASC = 'ASC';

	/** @const desc order */
	const DESC = 'DESC';

	/**
	 * Returns IEntity filtered by conditions.
	 * @param  array $where
	 * @return IEntity|null
	 */
	public function getBy(array $where);

	/**
	 * Returns entity collection filtered by conditions.
	 * Returns new instance of collection.
	 * @param  array $where
	 * @return ICollection
	 */
	public function findBy(array $where);

	/**
	 * Selects columns to order by.
	 * Returns new instance of collection.
	 * @param  string|array $column column name or array of column names
	 * @param  string       $direction sorting direction self::ASC or self::DESC
	 * @return ICollection
	 */
	public function orderBy($column, $direction = self::ASC);

	/**
	 * Resets collection ordering.
	 * @return ICollection
	 */
	public function resetOrderBy();

	/**
	 * Limits number of rows.
	 * @param  int  $limit
	 * @param  int  $offset
	 * @return ICollection
	 */
	public function limitBy($limit, $offset = null);

	/**
	 * Fetches the first row.
	 * @return IEntity|null
	 */
	public function fetch();

	/**
	 * Fetches all records.
	 * @return IEntity[]
	 */
	public function fetchAll();

	/**
	 * Fetches all records like $key => $value pairs.
	 * @param  string  $key associative key
	 * @param  string  $value value
	 * @return array
	 */
	public function fetchPairs($key = null, $value = null);

	/**
	 * @param  IEntity|null $parent
	 * @return Traversable
	 */
	public function getEntityIterator(IEntity $parent = null);

	/**
	 * @param  IEntity|null $parent
	 * @return int
	 */
	public function getEntityCount(IEntity $parent = null);

	/**
	 * Sets relationship mapping over collection.
	 * @internal
	 * @ignore
	 * @param  IRelationshipMapper|null $mapper
	 * @param  IEntity|null             $parent
	 * @return self
	 */
	public function setRelationshipMapping(IRelationshipMapper $mapper = null, IEntity $parent = null);

	/**
	 * @internal
	 * @ignore
	 * @return IRelationshipMapper
	 */
	public function getRelationshipMapper();

	/**
	 * Counts collection entities without fetching them from storage.
	 * @return int
	 */
	public function countStored();
}


code:
<?php

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

namespace Nextras\Orm\Collection;

interface IEntityContainer extends IEntityPreloadContainer
{
	/**
	 * Returms entity by joining key.
	 * @param int   $key
	 */
	public function getEntity($key);
}


code:
<?php

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

namespace Nextras\Orm\Collection;

interface IEntityIterator extends IEntityPreloadContainer, \Iterator, \Countable
{
	/**
	 * Sets index for inner hasMany collections.
	 * @param int|null  $index
	 */
	public function setDataIndex($index);
}


code:
<?php

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

namespace Nextras\Orm\Collection;

interface IEntityPreloadContainer
{
	/**
	 * Returns array of $property values for preloading.
	 * @param  string   $property
	 * @return array
	 */
	public function getPreloadValues($property);
}