<?php
namespace Squid\MySql\Impl\Command;

use Squid\MySql\Command\ICmdCreate;
use Squid\MySql\Command\IMySqlCommandConstructor;
use Squid\MySql\Impl\Command\Create\KeysCollection;
use Squid\MySql\Impl\Command\Create\ColumnsCollection;

class CmdCreate extends AbstractCommand implements ICmdCreate
{
	use \Squid\MySql\Impl\Traits\CmdTraits\TWithIndex;
	use \Squid\MySql\Impl\Traits\CmdTraits\TWithCreateColumns;

	const PART_TEMP			= 0;
	const PART_IF_NOT_EXIST	= 1;
	const PART_DB			= 2;
	const PART_NAME			= 3;
	const PART_ENGINE		= 4;
	const PART_CHARSET		= 5;
	const PART_AUTO_INC		= 6;
	const PART_COMMENT 		= 7;
	const PART_LIKE			= 8;
	const PART_AS			= 9;

	/**
	 * @var array Collection of default values.
	 */
	private static $DEFAULT = array(
		CmdCreate::PART_TEMP 			=> false,
		CmdCreate::PART_IF_NOT_EXIST	=> false,
		CmdCreate::PART_DB				=> '',
		CmdCreate::PART_NAME			=> false,
		CmdCreate::PART_ENGINE			=> false,
		CmdCreate::PART_CHARSET 		=> false,
		CmdCreate::PART_AUTO_INC 		=> false,
		CmdCreate::PART_COMMENT 		=> false,
		CmdCreate::PART_LIKE			=> false,
		CmdCreate::PART_AS				=> false
	);

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

	/**
	 * @return string
	 */
	private function getAsExpression()
	{
		$data = $this->parts[self::PART_AS];

		if ($data instanceof IMySqlCommandConstructor)
			return $data->assemble();

		return $data;
	}

	/**
	 * @param int $part
	 * @param string $prefix
	 * @param string $suffix
	 * @return string
	 */
	private function getPartIfSet($part, $prefix = '', $suffix = '')
	{
		return ($this->parts[$part] !== false ? $prefix . $this->parts[$part] . $suffix . ' ' : '');
	}

	public function __construct()
	{
		$this->indexes = new KeysCollection();
		$this->columnsList = new ColumnsCollection();
		$this->parts = self::$DEFAULT;
	}

	/**
	 * @return string
	 */
	public function getName()
	{
		return $this->parts[self::PART_NAME];
	}

	/**
	 * @return static
	 */
	public function temporary()
	{
		$this->parts[self::PART_TEMP] = 'TEMPORARY';
		return $this;
	}

	/**
	 * @param string $db
	 * @return static
	 */
	public function db($db)
	{
		$this->parts[self::PART_DB] = ($db ? "`$db`." : '');
		return $this;
	}

	/**
	 * @param string $name Database name, or table name if second parameter is omitted.
	 * @param string|bool $tableName
	 * @return static
	 */
	public function table($name, $tableName = false)
	{
		if ($tableName)
		{
			$this->db($name);
			$name = $tableName;
		}

		$this->parts[self::PART_NAME] = "`$name`";
		return $this;
	}

	/**
	 * @return static
	 */
	public function ifNotExists()
	{
		$this->parts[self::PART_IF_NOT_EXIST] = 'IF NOT EXISTS';
		return $this;
	}

	/**
	 * @param string $engine
	 * @return static
	 */
	public function engine($engine)
	{
		$this->parts[self::PART_ENGINE] = $engine;
		return $this;
	}

	/**
	 * @return static
	 */
	public function innoDB()
	{
		return $this->engine('InnoDB');
	}

	/**
	 * @param string $charset
	 * @return static
	 */
	public function charset($charset)
	{
		$this->parts[self::PART_CHARSET] = $charset;
		return $this;
	}

	/**
	 * @param string $comment
	 * @return static
	 */
	public function comment($comment)
	{
		$this->parts[self::PART_COMMENT] = '"' . $comment . '"';
		return $this;
	}

	/**
	 * @return array
	 */
	public function bind()
	{
		$as = $this->parts[self::PART_AS];
		return ($as instanceof IMySqlCommandConstructor ? $as->bind() : []);
	}

	/**
	 * Generate the query string.
	 * @return string Currently set query.
	 */
	public function assemble()
	{
		$command =
			'CREATE ' .
				$this->getPartIfSet(self::PART_TEMP) .
			'TABLE ' .
				$this->getPartIfSet(self::PART_IF_NOT_EXIST) .
				$this->parts[self::PART_DB] . $this->parts[self::PART_NAME];

		if ($this->parts[self::PART_LIKE])
		{
			return $command . ' ' . $this->parts[self::PART_LIKE];
		}

		if (!$this->columnsList->isEmpty())
		{
			$command .= '(';

			$columns = $this->columnsList->assemble();
			$keys = $this->indexes->assemble();
			$combined = array_merge($columns, $keys);
			$command .= implode(',', $combined);

			$command .= ') ' .
				$this->getPartIfSet(self::PART_ENGINE, 'ENGINE=') .
				$this->getPartIfSet(self::PART_CHARSET, 'CHARSET=') .
				$this->getPartIfSet(self::PART_CHARSET, 'AUTO_INCREMENT=') .
				$this->getPartIfSet(self::PART_COMMENT, 'COMMENT=');
		}

		if ($this->parts[self::PART_AS])
		{
			$command .= " {$this->getAsExpression()}";
		}

		return $command;
	}

	/**
	 * @param string $name Database name, or table name if second parameter is omitted.
	 * @param string|bool $tableName
	 * @return static
	 */
	public function like($name, $tableName = false)
	{
		$this->parts[self::PART_LIKE] = ($tableName ? "LIKE `$name`.`$tableName`" : "LIKE `$name`");
		return $this;
	}

	/**
	 * @param IMySqlCommandConstructor|string $query
	 * @return static
	 */
	public function asQuery($query)
	{
		$this->parts[self::PART_AS] = $query;
		return $this;
	}

	/**
	 * @param int $value
	 * @return static
	 */
	public function autoIncrement($value = 1)
	{
		$this->parts[self::PART_AUTO_INC] = $value;
		return $this;
	}
}


<?php
namespace Squid\MySql\Impl\Command;

use Squid\MySql\Command\ICmdDelete;
use Squid\MySql\Command\IWithLimit;
use Squid\Exceptions\SquidException;

class CmdDelete extends PartsCommand implements ICmdDelete
{
	use \Squid\MySql\Impl\Traits\CmdTraits\TDml;
	use \Squid\MySql\Impl\Traits\CmdTraits\TWithWhere;
	use \Squid\MySql\Impl\Traits\CmdTraits\TWithLimit;

	const PART_FROM		= 0;
	const PART_WHERE	= 1;
	const PART_ORDER_BY	= 2;
	const PART_LIMIT	= 3;

	/**
	 * @var array Collection of default values.
	 */
	private static $DEFAULT = [
		CmdDelete::PART_FROM	=> false,
		CmdDelete::PART_WHERE	=> false,
		CmdDelete::PART_ORDER_BY=> false,
		CmdDelete::PART_LIMIT	=> false
	];

	/**
	 * Get the parts this query can have.
	 * @return array Array containing only the part as keys and values set to false.
	 */
	protected function getDefaultParts()
	{
		return CmdDelete::$DEFAULT;
	}

	/**
	 * Combine all the parts into one sql.
	 * @return string Created query.
	 */
	protected function generate()
	{
		return
			'DELETE FROM ' . $this->getPart(CmdDelete::PART_FROM) . ' ' .
			Assembly::appendWhere($this->getPart(CmdDelete::PART_WHERE), true) .
			Assembly::appendOrderBy($this->getPart(CmdDelete::PART_ORDER_BY)) .
			Assembly::append('LIMIT', $this->getPart(CmdDelete::PART_LIMIT));
	}

	/**
	 * Set the table to delete from.
	 * @param string $table Name of the table to delete from.
	 * @return static
	 */
	public function from($table)
	{
		return $this->setPart(CmdDelete::PART_FROM, $table);
	}

	/**
	 * Add additional where clause.
	 * @param string $exp Expression to append.
	 * @param mixed|array|null $bind
	 * @return static
	 */
	public function where($exp, $bind = false)
	{
		return $this->appendPart(CmdDelete::PART_WHERE, $exp, $bind);
	}

	/**
	 * Required by the TLimit trait.
	 * @param array $columns
	 * @return static
	 */
	public function _orderBy(array $columns)
	{
		return $this->appendPart(CmdDelete::PART_ORDER_BY, $columns);
	}

	/**
	 * @param int $from Zero based index.
	 * @param int $count
	 * @return static|IWithLimit
	 */
	public function limit($from, $count): IWithLimit
	{
		if ($from)
			throw new SquidException('MySQL DELETE query supports only LIMIT [count] and not LIMIT [from], [count]');

		return $this->setPart(CmdDelete::PART_LIMIT, [$count], []);
	}
}


<?php
namespace Squid\MySql\Impl\Command;

use Squid\MySql\Command\ICmdController;

class CmdController extends AbstractCommand implements ICmdController
{
	use \Squid\MySql\Impl\Traits\CmdTraits\TDml;
	use \Squid\MySql\Impl\Traits\CmdTraits\TQuery;

	private $command;
	private $bind;

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

	public function assemble()
	{
		return $this->command;
	}

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

	/**
	 * Get the last inserted ID.
	 * @return mixed Last inserted auto increment ID.
	 */
	public function lastId()
	{
		$this->command	= 'SELECT LAST_INSERT_ID()';
		$this->bind		= array();

		return $this->queryInt();
	}

	/**
	 * Start a new transaction.
	 * @see https://dev.mysql.com/doc/refman/5.7/en/commit.html
	 * @return bool True on success; otherwise false.
	 */
	public function startTransaction()
	{
		// TODO: Start transaction
		$this->isInTransaction = true;

		return true;
	}

	/**
	 * Commit current transaction.
	 * @see https://dev.mysql.com/doc/refman/5.7/en/commit.html
	 * @return bool True on success; otherwise false.
	 */
	public function commit()
	{
		// TODO: End transaction
		$this->isInTransaction = false;

		return true;
	}

	/**
	 * Rollback current transaction.
	 * @see https://dev.mysql.com/doc/refman/5.7/en/commit.html
	 * @return bool True on success; otherwise false.
	 */
	public function rollback()
	{
		// TODO: Rollback
		$this->isInTransaction = false;

		return true;
	}

	/**
	 * Is there a pending transaction right now, waiting to be committed.
	 * @return bool Is there a transaction pending.
	 */
	public function isInTransaction()
	{
		return $this->isInTransaction;
	}

	/**
	 * @param string $tableName
	 * @return array|bool
	 */
	public function describe($tableName)
	{
		$this->command	= 'DESCRIBE ' . $tableName;
		$this->bind		= array();

		return $this->queryAll(true);
	}

	/**
	 * @param array $tables Assoc array of tables, were key is old name and value is new name
	 * @return bool
	 */
	public function rename($tables)
	{
		$this->command	= 'RENAME TABLE ';
		$this->bind		= array();

		$renames = array();

		foreach ($tables as $oldName => $newName) {
			$renames[] = "`$oldName` TO `$newName`";
		}

		$this->command .= implode(',', $renames);

		return $this->executeDml();
	}

	/**
	 * Rename tableA to tableB and tableB to tableA.
	 * @param string $tableA Name of the first table.
	 * @param string $tableB Name of the second table.
	 * @return mixed
	 */
	public function rotate($tableA, $tableB)
	{
		$tableT = $tableA . '_' . time() . '_' . rand(0, 1000000);

		return $this->rename([
			$tableB => $tableT,
			$tableA => $tableB,
			$tableT => $tableA
		]);
	}
}


<?php
namespace Squid\MySql\Impl\Command;

use Squid\MySql\Impl\Command\MultiQuery\StatementResult;
use Squid\MySql\Command\ICmdMultiQuery;
use Squid\MySql\Command\IMySqlCommandConstructor;
use Squid\MySql\Command\MultiQuery\IStatementResult;
use Squid\MySql\Exceptions\MySqlException;

class CmdMultiQuery extends AbstractCommand implements ICmdMultiQuery
{
	private $command = '';
	private $bind = [];

	/**
	 * @param string|IMySqlCommandConstructor|array $query
	 * @param array $bind Will be ignored if array of strings passed
	 * @return ICmdMultiQuery
	 */
	public function add($query, array $bind = [])
	{
		if (is_array($query))
		{
			foreach ($query as $item)
			{
				$this->add($item);
			}

			return $this;
		}
		else if ($query instanceof IMySqlCommandConstructor)
		{
			$bind = ($bind ?: $query->bind());
			$query = $query->assemble();
		}

		$this->command .= "$query;";
		$this->bind = array_merge($this->bind, $bind);

		return $this;
	}

	/**
	 * @return IStatementResult|false Result of the last query returned
	 */
	public function executeAll()
	{
		foreach ($this->executeIterator() as $result) {}

		if (!isset($result))
		{
			return false;
		}

		return $result;
	}

	private function checkForError(\PDOStatement $statement)
	{
		if ($statement->errorCode() != '00000')
			MySqlException::create($statement->errorInfo());
	}

	/**
	 * Return an iterator to iterate over all result sets.
	 * @return \Generator|IStatementResult[]
	 */
	public function executeIterator()
	{
		$result = $this->execute();

		if (!$result)
			throw new MySqlException('Could not execute multiset query!');

			while (true)
			{
				yield new StatementResult($result);

				if (!$result->nextRowset())
				{
					$this->checkForError($result);
					break;
				}
			}
	}

	/**
	 * @param callable $callback Called for each single query.
	 * The callback receives the IStatementResult.
	 */
	public function executeWithCallback(callable $callback)
	{
		foreach ($this->executeIterator() as $result)
		{
			$callbackResult = $callback($result);

			if ($callbackResult === false)
			{
				break;
			}
		}
	}

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

	/**
	 * Generate the query string.
	 * @return string Currently set query.
	 */
	public function assemble()
	{
		return $this->command;
	}
}


<?php
namespace Squid\MySql\Impl\Connectors\Object\Join\Selector;

use Squid\Exceptions\SquidException;
use Squid\MySql\Connectors\Object\Join\IJoinConnector;
use Squid\MySql\Connectors\Object\Query\ICmdObjectSelect;

use Squid\MySql\Impl\Connectors\Utils\Select\SelectDecorator;

use Structura\Map;

class JoinedObjectSelect extends SelectDecorator implements ICmdObjectSelect
{
	/** @var ICmdObjectSelect */
	private $child;

	/** @var IJoinConnector */
	private $config;

	public function __construct(ICmdObjectSelect $child, IJoinConnector $config)
	{
		parent::__construct($child);
		$this->config = $config;
		$this->child = $child;
	}

	/**
	 * @return mixed
	 */
	public function queryAll()
	{
		$data = $this->child->queryAll();
		return ($data ? $this->config->loaded($data) : $data);
	}

	/**
	 * @return mixed
	 */
	public function queryFirst()
	{
		$data = $this->child->queryFirst();
		return ($data ? $this->config->loaded($data) : $data);
	}

	/**
	 * @return mixed
	 */
	public function queryOne()
	{
		$data = $this->child->queryFirst();
		return ($data ? $this->config->loaded($data) : $data);
	}

	/**
	 * @param callable $callback Called for each selected row.
	 * If callback returns false, queryWithCallback will abort and return false.
	 * If callback returns 0, queryWithCallback will abort and return true.
	 * For any other value, callback will continue to the next row.
	 * @return bool
	 */
	public function queryWithCallback($callback)
	{
		throw new SquidException('queryWithCallback is not supported for joined table');
	}

	/**
	 * Return an iterator to iterate over all found objects.
	 */
	public function queryIterator(): iterable
	{
		throw new SquidException('queryIterator is not supported for joined table');
	}

	/**
	 * Return an array where the result of one column is the index and loaded object is the value.
	 * @param string $key Name of the key column.
	 * @param bool $removeColumnFromRow Should remove the key column from values before converting them to objects.
	 * @return array|false
	 */
	public function queryMapRow(string $key, $removeColumnFromRow = false)
	{
		$data = $this->child->queryMapRow($key, $removeColumnFromRow);

		if ($data)
		{
			$this->config->loaded($data);
		}

		return $data;
	}

	/**
	 * Return array where each value is an array of rows grouped by a single column.
	 * @param string|int $byColumn Column to group by.
	 * @param bool $removeColumn If set to true, the group by column is removed from the row.
	 * @return Map
	 */
	public function queryGroupBy($byColumn, bool $removeColumn = false): Map
	{
		$map = $this->child->queryGroupBy($byColumn, $removeColumn);

		if ($map->hasElements())
		{
			$allGroups = $map->toArray();
			$allObjects = array_merge(...$allGroups);

			$this->config->loaded($allObjects);
		}

		return $map;
	}
}


<?php
namespace Squid\MySql\Config;

use Squid\MySql\Impl\Connection\MySqlConnection;
use Squid\Exceptions\SquidException;

class ConfigCollection
{
	/** @var MySqlConnectionConfig[] */
	private $configs = [];

	/** @var IConfigLoader*/
	private $configLoader;

	/**
	 * @param IConfigLoader $loader
	 */
	public function __construct(IConfigLoader $loader)
	{
		$this->configLoader = $loader;
	}

	/**
	 * @return MySqlConnectionConfig[]
	 */
	public function getAllConnections(): array
	{
		return array_values($this->configs);
	}

	public function add(string $name, MySqlConnectionConfig $connection)
	{
		if (isset($this->configs[$name]))
			throw new SquidException("Config $name already exists");

		$this->configs[$name] = $connection;
	}

	/**.
	 * @param string $name
	 * @return MySqlConnectionConfig
	 */
	public function get($name): MySqlConnectionConfig
	{
		if (!isset($this->configs[$name]))
			$this->configs[$name] = $this->configLoader->getConfig($name);

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

	public function has(string $name): bool
	{
		return isset($this->configs[$name]) || $this->configLoader->hasConfig($name);
	}
}


<?php
namespace Squid\MySql\Impl\Command;

use Squid\MySql\Command\ICmdUpsert;

class CmdUpsert extends CmdInsert implements ICmdUpsert
{
	use \Squid\MySql\Impl\Traits\CmdTraits\TWithSet;

	/**
	 * @var array Collection of default values.
	 */
	private static $DEFAULT;

	/**
	 * @var int Index of the where clause.
	 */
	private static $PART_SET;

	/**
	 * Get the parts this query can have.
	 * @return array Array containing only the part as keys and values set to false.
	 */
	protected function getDefaultParts()
	{
		if (!isset(CmdUpsert::$DEFAULT))
		{
			CmdUpsert::$DEFAULT		= parent::getDefaultParts();
			CmdUpsert::$PART_SET	= count(CmdUpsert::$DEFAULT);

			CmdUpsert::$DEFAULT[CmdUpsert::$PART_SET] = false;
		}

		return CmdUpsert::$DEFAULT;
	}

	/**
	 * Combine all the parts into one sql.
	 * @return string Created query.
	 */
	protected function generate()
	{
		return
			parent::generate() .
			Assembly::append(
				'ON DUPLICATE KEY UPDATE',
				$this->getPart(CmdUpsert::$PART_SET),
				', ');
	}

	/**
	 * @inheritdoc
	 */
	public function setUseNewValues($fields)
	{
		if (!is_array($fields)) $fields = [$fields];

		foreach ($fields as $field)
		{
			$this->setExp("`$field`", "VALUES(`$field`)");
		}

		return $this;
	}

	/**
	 * @inheritdoc
	 */
	public function setDuplicateKeys($fields)
	{
		return $this->setUseNewValues(
			array_diff(
				$this->getFields(),
				(is_array($fields) ? $fields : [$fields])
			));
	}

	/**
	 * @inheritdoc
	 */
	public function _set($exp, $bind = false)
	{
		return $this->appendPart(CmdUpsert::$PART_SET, $exp, $bind);
	}
}


<?php
namespace Squid\MySql\Impl\Extensions\Combine\Select;

use Objection\LiteObject;
use Structura\Map;

use Squid\OrderBy;

use Squid\MySql\Command\IWithLimit;
use Squid\MySql\Command\ICmdSelect;
use Squid\MySql\Command\IWithExtendedWhere;
use Squid\MySql\Command\IMySqlCommandConstructor;
use Squid\MySql\Connection\IMySqlConnection;

class SelectCombiner implements ICmdSelect
{
	/** @var ICmdSelect[] */
	private $selects = [];

	/**
	 * @param string $function
	 * @param array ...$args
	 * @return static
	 */
	private function invokeOnAll($function, ...$args)
	{
		foreach ($this->selects as $select)
		{
			call_user_func([$select, $function], ...$args);
		}

		return $this;
	}

	/**
	 * @param string $function
	 * @param array ...$args
	 * @return mixed
	 */
	private function invokeOnUnion($function, ...$args)
	{
		$all = $this->unionCommands();
		return call_user_func([$all, $function], ...$args);
	}

	public function __clone()
	{
		$clonedSelects = [];

		foreach ($this->selects as $key => $select)
		{
			$clonedSelects[$key] = clone $select;
		}

		$this->selects = $clonedSelects;
	}

	/**
	 * @param ICmdSelect $select
	 */
	public function add(ICmdSelect $select)
	{
		$this->selects[] = $select;
	}

	/**
	 * @param string|int $key
	 * @param ICmdSelect $select
	 */
	public function addByKey($key, ICmdSelect $select)
	{
		$this->selects[$key] = $select;
	}

	/**
	 * @param string|int $key
	 * @return ICmdSelect
	 */
	public function getByKey($key)
	{
		return $this->selects[$key];
	}

	/**
	 * @param string|int $key
	 * @return ICmdSelect
	 */
	public function removeByKey($key)
	{
		$select = $this->selects[$key];
		unset($this->selects[$key]);
		return $select;
	}

	/**
	 * @param string $key
	 * @return bool
	 */
	public function has($key)
	{
		return isset($this->selects[$key]);
	}

	public function unionCommands(): ?ICmdSelect
	{
		/** @var ICmdSelect|null $main */
		$main = null;

		foreach ($this->selects as $select)
		{
			if (is_null($main)) $main = clone $select;
			else $main->union($select);
		}

		return $main;
	}

	// Calls on each select

	public function distinct($distinct = true) { return $this->invokeOnAll(__FUNCTION__, ...func_get_args()); }
	public function column(...$columns) { return $this->invokeOnAll(__FUNCTION__, ...func_get_args()); }
	public function columns($columns, $table = false) { return $this->invokeOnAll(__FUNCTION__, ...func_get_args()); }
	public function columnsExp($columns, $bind = false) { return $this->invokeOnAll(__FUNCTION__, ...func_get_args()); }
	public function columnAs($column, $alias) { return $this->invokeOnAll(__FUNCTION__, ...func_get_args()); }
	public function columnAsExp($column, $alias, $bind = false) { return $this->invokeOnAll(__FUNCTION__, ...func_get_args()); }
	public function from($table, $alias = false) { return $this->invokeOnAll(__FUNCTION__, ...func_get_args()); }
	public function join($table, $alias, $condition, $bind = false) { return $this->invokeOnAll(__FUNCTION__, ...func_get_args()); }
	public function leftJoin($table, $alias, $condition, $bind = false, $outer = false) { return $this->invokeOnAll(__FUNCTION__, ...func_get_args()); }
	public function rightJoin($table, $alias, $condition, $bind = false, $outer = false) { return $this->invokeOnAll(__FUNCTION__, ...func_get_args()); }
	public function groupBy($column, $bind = false) { return $this->invokeOnAll(__FUNCTION__, ...func_get_args()); }
	public function having($exp, $bind = false) { return $this->invokeOnAll(__FUNCTION__, ...func_get_args()); }
	public function withRollup($withRollup = true) { return $this->invokeOnAll(__FUNCTION__, ...func_get_args()); }
	public function union(IMySqlCommandConstructor $select, $all = false) { return $this->invokeOnAll(__FUNCTION__, ...func_get_args()); }
	public function unionAll(IMySqlCommandConstructor $select) { return $this->invokeOnAll(__FUNCTION__, ...func_get_args()); }
	public function forUpdate($forUpdate = true) { return $this->invokeOnAll(__FUNCTION__, ...func_get_args()); }
	public function lockInShareMode($lockInShareMode = true) { return $this->invokeOnAll(__FUNCTION__, ...func_get_args()); }
	public function setConnection(IMySqlConnection $conn) { return $this->invokeOnAll(__FUNCTION__, ...func_get_args()); }
	public function limit($from, $count): IWithLimit { return $this->invokeOnAll(__FUNCTION__, ...func_get_args()); }
	public function limitBy($count): IWithLimit { return $this->invokeOnAll(__FUNCTION__, ...func_get_args()); }
	public function page($page, $pageSize): IWithLimit { return $this->invokeOnAll(__FUNCTION__, ...func_get_args()); }
	public function orderBy($column, $type = OrderBy::ASC): IWithLimit { return $this->invokeOnAll(__FUNCTION__, ...func_get_args()); }
	public function byId($value) { return $this->invokeOnAll(__FUNCTION__, ...func_get_args()); }
	public function byField($field, $value) { return $this->invokeOnAll(__FUNCTION__, ...func_get_args()); }
	public function byFields($fields, $values = null) { return $this->invokeOnAll(__FUNCTION__, ...func_get_args()); }
	public function where($exp, $bind = false) { return $this->invokeOnAll(__FUNCTION__, ...func_get_args()); }
	public function whereIn($field, $values, $negate = false) { return $this->invokeOnAll(__FUNCTION__, ...func_get_args()); }
	public function whereNotIn($field, $values) { return $this->invokeOnAll(__FUNCTION__, ...func_get_args()); }
	public function whereExists(ICmdSelect $select, $negate = false) { return $this->invokeOnAll(__FUNCTION__, ...func_get_args()); }
	public function whereNotExists(ICmdSelect $select) { return $this->invokeOnAll(__FUNCTION__, ...func_get_args()); }
	public function whereBetween(string $field, $greater, $less): IWithExtendedWhere { return $this->invokeOnAll(__FUNCTION__, ...func_get_args()); }
	public function whereNotEqual(string $field, $value): IWithExtendedWhere { return $this->invokeOnAll(__FUNCTION__, ...func_get_args()); }
	public function whereLess(string $field, $value): IWithExtendedWhere { return $this->invokeOnAll(__FUNCTION__, ...func_get_args()); }
	public function whereLessOrEqual(string $field, $value): IWithExtendedWhere { return $this->invokeOnAll(__FUNCTION__, ...func_get_args()); }
	public function whereGreater(string $field, $value): IWithExtendedWhere { return $this->invokeOnAll(__FUNCTION__, ...func_get_args()); }
	public function whereGreaterOrEqual(string $field, $value): IWithExtendedWhere { return $this->invokeOnAll(__FUNCTION__, ...func_get_args()); }

	// Calls on union of all selects

	public function bind() { return $this->invokeOnUnion(__FUNCTION__, ...func_get_args()); }
	public function assemble() { return $this->invokeOnUnion(__FUNCTION__, ...func_get_args()); }
	public function execute() { return $this->invokeOnUnion(__FUNCTION__, ...func_get_args()); }
	public function query() { return $this->invokeOnUnion(__FUNCTION__, ...func_get_args()); }
	public function queryAll($isAssoc = false) { return $this->invokeOnUnion(__FUNCTION__, ...func_get_args()); }
	public function queryRow($isAssoc = false, $expectOne = true) { return $this->invokeOnUnion(__FUNCTION__, ...func_get_args()); }
	public function queryColumn($expectOne = true) { return $this->invokeOnUnion(__FUNCTION__, ...func_get_args()); }
	public function queryScalar($default = false, $expectOne = true) { return $this->invokeOnUnion(__FUNCTION__, ...func_get_args()); }
	public function queryInt($expectOne = true) { return $this->invokeOnUnion(__FUNCTION__, ...func_get_args()); }
	public function queryBool($expectOne = true) { return $this->invokeOnUnion(__FUNCTION__, ...func_get_args()); }
	public function queryWithCallback($callback, $isAssoc = true) { return $this->invokeOnUnion(__FUNCTION__, ...func_get_args()); }
	public function queryIterator($isAssoc = true) { return $this->invokeOnUnion(__FUNCTION__, ...func_get_args()); }
	public function queryMap($key = 0, $value = 1) { return $this->invokeOnUnion(__FUNCTION__, ...func_get_args()); }
	public function queryObject(string $className): ?LiteObject { return $this->invokeOnUnion(__FUNCTION__, ...func_get_args()); }
	public function queryObjects(string $className): array { return $this->invokeOnUnion(__FUNCTION__, ...func_get_args()); }
	public function queryMapRow($key = 0, $removeColumnFromRow = false) { return $this->invokeOnUnion(__FUNCTION__, ...func_get_args()); }
	public function queryGroupBy($byColumn, bool $removeColumn = false): Map { return $this->invokeOnUnion(__FUNCTION__, ...func_get_args()); }
	public function queryCount() { return $this->invokeOnUnion(__FUNCTION__, ...func_get_args()); }

	/**
	 * Execute a SELECT EXISTS On each query. Will stop when at least one query returns true.
	 * @return bool|null Null on error
	 */
	public function queryExists()
	{
		foreach ($this->selects as $select)
		{
			$result = $select->queryExists();

			if (is_null($result) || $result)
			{
				return $result;
			}
		}

		return false;
	}

	public function queryLimit(int $totalLimit, $isAssoc = true)
	{
		$result = [];

		foreach ($this->selects as $select)
		{
			$query = clone $select;
			$query->limitBy($totalLimit);

			$queryResult = $select->queryAll($isAssoc);
			$totalLimit -= count($queryResult);

			$result = array_merge($queryResult);

			if ($totalLimit <= 0)
				break;
		}

		return $result;
	}

	/**
	 * For debug only
	 * @return string Return string in format: "Query string : {json bind params}"
	 */
	public function __toString()
	{
		$result = 'Union of: ';

		foreach ($this->selects as $key => $select)
		{
			if ($result)
				$result .= ', ';

			$result .= "$key: [ {$select->__toString()} ]";
		}

		return $result;
	}
}


<?php
namespace Squid\MySql\Impl\Command;

use Squid\MySql\Command\ICmdInsert;
use Squid\MySql\Command\ICmdSelect;
use Squid\Exceptions\SquidException;

class CmdInsert extends PartsCommand implements ICmdInsert
{
	use \Squid\MySql\Impl\Traits\CmdTraits\TDml;

	const PART_IGNORE	= 0;
	const PART_INTO		= 1;
	const PART_AS		= 2;
	const PART_VALUES	= 3;

	/**
	 * @var array Collection of default values.
	 */
	private static $DEFAULT = [
		CmdInsert::PART_IGNORE	=> false,
		CmdInsert::PART_INTO	=> false,
		CmdInsert::PART_AS		=> false,
		CmdInsert::PART_VALUES	=> false
	];

	private $placeholder	= '';

	/**
	 * @var array|bool False if not set; otherwise array of fields to insert into.
	 */
	private $fields = false;

	/** @var array|bool */
	private $default = false;

	/**
	 * Any value not present in the values set, and with a default value,
	 * should be appended to the given value set.
	 * @param array $values Array of values for a single row to modify.
	 */
	private function fixDefaultValues(&$values)
	{
		if (!$this->default) return;

		$values = array_merge($this->default, $values);
	}

	/**
	 * Append associative array of values.
	 * @param array $values Key is field name.
	 * @return static
	 */
	private function appendByField($values)
	{
		$fixed = array();

		foreach ($this->fields as $field)
		{
			$fixed[] = $values[$field];
		}

		return $this->appendByPosition($fixed);
	}

	/**
	 * Append numeric array of values.
	 * @param array $values Numeric array of values where position of each value matches the
	 * position of it's filed in $fields private data member.
	 * @return static
	 */
	private function appendByPosition($values)
	{
		$this->setPart(CmdInsert::PART_AS, false);

		if (!$this->placeholder)
			$this->placeholder = Assembly::placeholder(count($values), true);

		return $this->appendPart(
			CmdInsert::PART_VALUES,
			$this->placeholder,
			$values
		);
	}

	/**
	 * Get the parts this query can have.
	 * @return array Array containing only the part as keys and values set to false.
	 */
	protected function getDefaultParts()
	{
		return CmdInsert::$DEFAULT;
	}

	/**
	 * Combined all the parts into one sql.
	 * @return string Created query.
	 */
	protected function generate()
	{
		if (!$this->getPart(CmdInsert::PART_INTO))
			throw new SquidException('Target table must be defined for Insert command. ' .
				'into() was not called or called with empty value');

		$command =  'INSERT ' .
			($this->getPart(CmdInsert::PART_IGNORE) ? 'IGNORE ' : '') .
			'INTO `' . $this->getPart(CmdInsert::PART_INTO) . '` ';

		if ($this->fields)
		{
			$command .= '(`' . implode('`,`', $this->fields) . '`) ';
		}

		$as = $this->getPart(CmdInsert::PART_AS);

		if ($as)
		{
			return $command . $as;
		}

		return $command . Assembly::append('VALUES', $this->getPart(CmdInsert::PART_VALUES), ', ');
	}

	/**
	 * Set the status of the ignore flag.
	 * @param bool $ignore If true, use ignore flag, otherwise don't.
	 * @return static
	 */
	public function ignore($ignore = true)
	{
		return $this->setPart(CmdInsert::PART_IGNORE, $ignore);
	}

	/**
	 * Set the table to select into.
	 * @param string $table Table name.
	 * @param array|null $fields Set of fields to insert data into. This can be ignored
	 * if set later using values with assoc array.
	 * @return static
	 */
	public function into($table, array $fields = null)
	{
		$this->setPart(CmdInsert::PART_INTO, $table);

		if (!is_null($fields))
		{
			$this->placeholder = false;
			$this->fields = $fields;
		}

		return $this;
	}

	/**
	 * Set the default values to use.
	 * @param array $default Assoc array of values where key is column name and value is
	 * the default value to use.
	 * @return static
	 */
	public function defaultValues(array $default)
	{
		$this->default = $default;
		return $this;
	}

	/**
	 * Append a set of values to insert.
	 * @param array $values If numeric array, values must match fields that were set earlier.
	 * If assoc array, key must be the field name. If defaultValues was called, any missing field value,
	 * (if fields where set earlier) will be checked to have a default value.
	 * @return static
	 */
	public function values($values)
	{
		if (isset($values[0]))
			return $this->appendByPosition($values);

		$this->fixDefaultValues($values);

		if (!$this->fields)
		{
			$this->placeholder = false;
			$this->fields = array_keys($values);
			return $this->appendByPosition(array_values($values));
		}

		return $this->appendByField($values);
	}

	/**
	 * Append a number of rows at once.
	 * @param array $valuesSet Numeric array were each value is array of values to insert into the table.
	 * For each row in the set, values function must be called, so all rules applying on the
	 * $values parameter in values function, must apply on each value in the $valuesSet array.
	 * @return static
	 */
	public function valuesBulk($valuesSet)
	{
		foreach ($valuesSet as $row)
		{
			$this->values($row);
		}

		return $this;
	}

	/**
	 * Insert data using given expression. That can be sued if some of the values are expressions.
	 * Expressions must be Sql safe.
	 * @param string $expression Expression to insert. Expression must start from ( and end with ).
	 * Expression can contain a number of rows to insert. It will not be validated in no way so number
	 * of fields must match number of table fields.
	 * @param mixed|array $bind Single bind param or array of bind params.
	 * @return static
	 */
	public function valuesExp($expression, $bind = false)
	{
		return $this->appendPart(
			CmdInsert::PART_VALUES,
			$expression,
			$bind
		);
	}

	/**
	 * Use select command to insert into the table.
	 * Note that in this case no values can be bind to the table.
	 * @param ICmdSelect $select Select sub query used to retrieve the insert values.
	 * @return static
	 */
	public function asSelect(ICmdSelect $select)
	{
		$this->setPart(CmdInsert::PART_VALUES, false);
		return $this->setPart(CmdInsert::PART_AS, $select->assemble(), $select->bind());
	}

	/**
	 * Get the fields set for this insert.
	 * @return array Array of fields.
	 */
	public function getFields()
	{
		return $this->fields;
	}
}


<?php
namespace Squid\MySql\Impl\Command;

use Squid\Exceptions\SquidException;

class Assembly
{
	use \Traitor\TStaticClass;

	/**
	 * @param array $values Expressions of the where clause
	 * @param bool $forceExist If true, $values must have at least one value
	 * @return string
	 * @throws SquidException
	 */
	public static function appendWhere($values, $forceExist = false)
	{
		if ($forceExist && !$values)
			throw new SquidException('WHERE clause must be present for this type of command!');

		return Assembly::append('WHERE', $values, ' AND ');
	}

	/**
	 * @param array $values Expressions of the order by clause
	 * @return string
	 */
	public static function appendOrderBy($values)
	{
		return Assembly::append('ORDER BY', $values, ', ');
	}

	/**
	 * Append set clause.
	 * @param array $values Expressions of the set clause
	 * @param bool $forceExist If true, $values must have at least one value
	 * @return string
	 * @throws SquidException
	 */
	public static function appendSet($values, $forceExist = false)
	{
		if ($forceExist && !$values)
			throw new SquidException('SET clause must be present for this type of command!');

		return Assembly::append('SET', $values, ', ');
	}

	/**
	 * @param bool $limit True if limit requested, otherwise false.
	 * @param array $bind
	 * @return string
	 */
	public static function appendLimit($limit, $bind)
	{
		if (!$limit) return '';

		return ((!is_array($bind) || count($bind) == 1) ? 'LIMIT ? ' : 'LIMIT ?, ? ');
	}

	/**
	 * @param string $prefix
	 * @param array|bool $values
	 * @param string $glue
	 * @return string
	 */
	public static function append($prefix, $values, $glue = ',')
	{
		return ($values ? $prefix . ' ' . implode($glue, $values) . ' ' : '');
	}

	/**
	 * @param array $values
	 * @param string $glue
	 * @return string
	 */
	public static function appendDirectly(array $values, $glue)
	{
		return ($values ? implode($glue, $values) . ' ' : '');
	}

	/**
	 * @param int $count Number of elements.
	 * @param bool $surround If true, generate () at start and end of the result.
	 * @param string $glue
	 * @return string
	 */
	public static function placeholder($count, $surround = false, $glue = ',')
	{
		$result = implode($glue, array_pad(array(), $count, '?'));
		return ($surround ? "($result)" : $result);
	}
}


<?php
namespace Squid\MySql\Impl\Command;

use Squid\MySql\Command\IMySqlCommandConstructor;
use Squid\MySql\Connection\IMySqlConnection;
use Squid\Exceptions\SquidException;

abstract class AbstractCommand implements IMySqlCommandConstructor
{
	/** @var IMySqlConnection */
	private $conn = null;

	/**
	 * @return IMySqlConnection|null
	 */
	protected function getConn()
	{
		return $this->conn;
	}

	protected function getVersion(): string
	{
		return $this->conn->version();
	}

	/**
	 * Set the connection to use.
	 * @param \Squid\MySql\Connection\IMySqlConnection $conn
	 */
	public function setConnection(IMySqlConnection $conn)
	{
		$this->conn = $conn;
	}

	/**
	 * Execute the generated query.
	 * @return \PDOStatement|null Result generated by the query, or null on fail.
	 * @throws SquidException
	 */
	public function execute()
	{
		if (is_null($this->conn))
			throw new SquidException("Can't execute query, implicitly created without connection!");

		$cmd = $this->assemble();
		$bind = $this->bind();

		return $this->conn->execute($cmd, $bind);
	}

	/**
	 * @return string
	 */
	public function __toString()
	{
		$bind = json_encode($this->bind());
		return "{$this->assemble()} <=> $bind";
	}
}


<?php
namespace Squid\MySql\Extensions\Enrichment;

use Objection\LiteObject;
use Structura\Map;
use Squid\MySql\Command\IQuery;

abstract class AbstractQueryEnrichment implements IQueryEnrichment
{
	/** @var IQuery */
	private $source;

	/**
	 * @return IQuery
	 */
	protected function getSource()
	{
		return $this->source;
	}

	/**
	 * @param IQuery $query
	 * @return static
	 */
	public function setSource(IQuery $query)
	{
		$this->source = $query;
		return $this;
	}

	/**
	 * Identical to queryAll(true);
	 * @return array
	 */
	public function query()
	{
		return $this->queryAll(true);
	}

	/**
	 * @param bool|int $isAssoc Will accept \PDO::FETCH_*
	 * @return array
	 */
	public function queryAll($isAssoc = false)
	{
		return $this->source->queryAll($isAssoc);
	}

	/**
	 * @param bool|int $isAssoc Will accept \PDO::FETCH_*
	 * @param bool $expectOne
	 * @return array|false
	 */
	public function queryRow($isAssoc = false, $expectOne = true)
	{
		return $this->source->queryRow($isAssoc, $expectOne);
	}

	/**
	 * @param bool $expectOne If true and more then one column is selected by the query, throw an exception.
	 * @return array|bool Numeric array of all the values in the first found row.
	 */
	public function queryColumn($expectOne = true)
	{
		return $this->source->queryColumn($expectOne);
	}

	/**
	 * @param mixed $default Default value to return if no results found.
	 * @param bool $expectOne If true and more then one column or row are
	 * selected by the query, throw an exception.
	 * @return mixed First column of the first row, or $default value if nothing found.
	 */
	public function queryScalar($default = false, $expectOne = true)
	{
		return $this->source->queryScalar($default, $expectOne);
	}

	/**
	 * @param bool $expectOne If true and more then one column or row are
	 * selected by the query, throw an exception.
	 * @return int|bool False on error.
	 */
	public function queryInt($expectOne = true)
	{
		return $this->source->queryInt($expectOne);
	}

	/**
	 * @param bool $expectOne If true and more then one column or row are
	 * selected by the query, throw an exception.
	 * @return bool|null Null on error.
	 */
	public function queryBool($expectOne = true)
	{
		return $this->source->queryBool($expectOne);
	}

	/**
	 * Execute a SELECT EXISTS (Current query)
	 * @return bool|null Null on error
	 */
	public function queryExists()
	{
		return $this->source->queryExists();
	}

	/**
	 * Execute SELECT COUNT(*). If the query is a group by query, number of distinct values is returned.
	 * @return int|bool
	 */
	public function queryCount()
	{
		return $this->source->queryCount();
	}

	/**
	 * @param callable $callback Called for each selected row.
	 * If callback returns false, queryWithCallback will abort and return false.
	 * If callback returns 0, queryWithCallback will abort and return true.
	 * For any other value, callback will continue to the next row.
	 * @param bool $isAssoc
	 * @return bool
	 */
	public function queryWithCallback($callback, $isAssoc = true)
	{
		return $this->source->queryWithCallback($callback, $isAssoc);
	}

	/**
	 * Return an iterator to iterate over all found rows.
	 * @param bool $isAssoc If true return assoc result; otherwise return numeric.
	 * Also integer value as \PDO::FETCH_* mode can be passed for other results.
	 * @return \Iterator
	 */
	public function queryIterator($isAssoc = true)
	{
		return $this->source->queryIterator($isAssoc);
	}

	/**
	 * Return an array where the result of one column is the index and the second is value.
	 * @param int|string $key Name of the key column.
	 * @param int|string $value Name of the value column
	 * @return array|false
	 */
	public function queryMap($key = 0, $value = 1)
	{
		return $this->source->queryMap($key, $value);
	}

	/**
	 * Return array where each value is an array of rows grouped by a single column.
	 * @param string|int $byColumn Column to group by.
	 * @param bool $removeColumn If set to true, the group by column is removed from the row.
	 * @return Map
	 */
	public function queryGroupBy($byColumn, bool $removeColumn = false): Map
	{
		return $this->source->queryGroupBy($byColumn, $removeColumn);
	}

	/**
	 * Return an array where the result of one column is the index and the remaining data is value.
	 * @param int|string $key Name of the key column.
	 * @param bool $removeColumnFromRow Should remove the key column from values.
	 * @return array|false
	 */
	public function queryMapRow($key = 0, $removeColumnFromRow = false)
	{
		return $this->source->queryMapRow($key, $removeColumnFromRow);
	}

	public function queryObject(string $className): ?LiteObject
	{
		return $this->source->queryObject($className);
	}

	public function queryObjects(string $className): array
	{
		return $this->source->queryObjects($className);
	}
}


<?php
namespace Squid\MySql\Impl\Connectors\Object\Query;

use Structura\Map;

use Squid\MySql\Connectors\Map\IRowMap;
use Squid\MySql\Connectors\Object\Query\IObjectSelector;
use Squid\MySql\Connectors\Object\Query\ICmdObjectSelect;

use Squid\MySql\Impl\Traits\CmdTraits\TWithLimit;
use Squid\MySql\Impl\Traits\CmdTraits\TWithColumn;
use Squid\MySql\Impl\Traits\CmdTraits\Decorators\TWithWhereDecorated;

use Squid\MySql\Impl\Connectors\Utils\Select\SelectDecorator;
use Squid\MySql\Impl\Connectors\Object\Query\Selectors\StandardSelector;

class CmdObjectSelect extends SelectDecorator implements ICmdObjectSelect
{
	use TWithLimit;
	use TWithColumn;
	use TWithWhereDecorated;

	/** @var IObjectSelector */
	private $selector;

	/**
	 * @param IObjectSelector|IRowMap $selector
	 */
	public function __construct($selector)
	{
		parent::__construct();

		if ($selector instanceof IRowMap)
		{
			$selector = new StandardSelector($selector);
		}

		$this->selector = $selector;
	}

	/**
	 * @return mixed
	 */
	public function queryAll()
	{
		return $this->selector->all($this->getSelect());
	}

	/**
	 * @return mixed
	 */
	public function queryFirst()
	{
		return $this->selector->first($this->getSelect());
	}

	/**
	 * @return mixed
	 */
	public function queryOne()
	{
		return $this->selector->one($this->getSelect());
	}

	/**
	 * @param callable $callback Called for each selected row.
	 * If callback returns false, queryWithCallback will abort and return false.
	 * If callback returns 0, queryWithCallback will abort and return true.
	 * For any other value, callback will continue to the next row.
	 * @return bool
	 */
	public function queryWithCallback($callback)
	{
		return $this->selector->withCallback($this->getSelect(), $callback);
	}

	/**
	 * Return an iterator to iterate over all found objects.
	 * @return iterable
	 */
	public function queryIterator(): iterable
	{
		return $this->selector->iterator($this->getSelect());
	}

	/**
	 * Return an array where the result of one column is the index and loaded object is the value.
	 * @param string $key Name of the key column.
	 * @param bool $removeColumnFromRow Should remove the key column from values before converting them to objects.
	 * @return array|false
	 */
	public function queryMapRow(string $key, $removeColumnFromRow = false)
	{
		return $this->selector->map($this->getSelect(), $key, $removeColumnFromRow);
	}

	/**
	 * Return array where each value is an array of rows grouped by a single column.
	 * @param string|int $byColumn Column to group by.
	 * @param bool $removeColumn If set to true, the group by column is removed from the row.
	 * @return Map
	 */
	public function queryGroupBy($byColumn, bool $removeColumn = false): Map
	{
		return $this->selector->groupBy($this->getSelect(), $byColumn, $removeColumn);
	}

	public function __clone()
	{
		parent::__clone();
		$this->selector = clone $this->selector;
	}
}


<?php
namespace Squid\MySql\Impl\Command;

use Squid\MySql\Command\ICmdUpdate;
use Squid\MySql\Command\IWithLimit;

class CmdUpdate extends PartsCommand implements ICmdUpdate
{
	use \Squid\MySql\Impl\Traits\CmdTraits\TDml;
	use \Squid\MySql\Impl\Traits\CmdTraits\TWithSet;
	use \Squid\MySql\Impl\Traits\CmdTraits\TWithWhere;
	use \Squid\MySql\Impl\Traits\CmdTraits\TWithLimit;

	const PART_IGNORE	= 0;
	const PART_TABLE	= 1;
	const PART_SET		= 2;
	const PART_WHERE	= 3;
	const PART_ORDER_BY	= 4;
	const PART_LIMIT	= 5;

	/**
	 * @var array Collection of default values.
	 */
	private static $DEFAULT = array(
		CmdUpdate::PART_IGNORE	=> false,
		CmdUpdate::PART_TABLE	=> false,
		CmdUpdate::PART_SET		=> false,
		CmdUpdate::PART_WHERE	=> false,
		CmdUpdate::PART_ORDER_BY=> false,
		CmdUpdate::PART_LIMIT	=> false
	);

	/**
	 * @return array Array containing only the part as keys and values set to false.
	 */
	protected function getDefaultParts()
	{
		return CmdUpdate::$DEFAULT;
	}

	/**
	 * Combined all the parts into one sql.
	 * @return string Created query.
	 */
	protected function generate()
	{
		return
			'UPDATE ' .
				($this->getPart(CmdUpdate::PART_IGNORE) ? 'IGNORE ' : '') .
				$this->getPart(CmdUpdate::PART_TABLE) . ' ' .
				Assembly::appendSet($this->getPart(CmdUpdate::PART_SET), true) .
				Assembly::appendWhere($this->getPart(CmdUpdate::PART_WHERE), true) .
				Assembly::appendOrderBy($this->getPart(CmdUpdate::PART_ORDER_BY)) .
				Assembly::appendLimit(
					$this->getPart(CmdUpdate::PART_LIMIT),
					$this->getBind(CmdUpdate::PART_LIMIT));
	}

	/**
	 * Set the status of the ignore flag.
	 * @param bool $ignore If true, use ignore flag, otherwise don't.
	 * @return static
	 */
	public function ignore($ignore = true)
	{
		return $this->setPart(CmdUpdate::PART_IGNORE, $ignore);
	}

	/**
	 * Set the table to update.
	 * @param string $table Name of the table to update.
	 * @return static
	 */
	public function table($table)
	{
		return $this->setPart(CmdUpdate::PART_TABLE, $table);
	}

	/**
	 * Add additional where clause.
	 * @param string $exp Expression to append.
	 * @param mixed|array|null $bind Single bind value, array of values or false if no
	 * bind values are needed for this expression.
	 * @return static
	 */
	public function where($exp, $bind = false)
	{
		return $this->appendPart(CmdUpdate::PART_WHERE, $exp, $bind);
	}

	/**
	 * Required by the TLimit trait.
	 * @param array $columns Array of expressions to order by.
	 * @return static
	 */
	public function _orderBy(array $columns)
	{
		return $this->appendPart(CmdUpdate::PART_ORDER_BY, $columns);
	}

	/**
	 * Limit the query for given set.
	 * @param int $from Select form this row.
	 * @param int $count Maximum number of rows to select.
	 * @return IWithLimit|static
	 */
	public function limit($from, $count): IWithLimit
	{
		return $this->setPart(CmdUpdate::PART_LIMIT, true, ($from ? array($from, $count) : $count));
	}

	/**
	 * Function called by TWithSet.
	 * @param string $exp Full set expression.
	 * @param mixed $bind Bind params, if any.
	 * @return static
	 */
	public function _set($exp, $bind = false)
	{
		return $this->appendPart(CmdUpdate::PART_SET, $exp, $bind);
	}
}


<?php
namespace Squid\MySql\Impl\Command;

use Squid\MySql\Command\ICmdSelect;
use Squid\MySql\Command\IWithLimit;
use Squid\MySql\Command\IMySqlCommandConstructor;

class CmdSelect extends PartsCommand implements ICmdSelect
{
	use \Squid\MySql\Impl\Traits\CmdTraits\TQuery;
	use \Squid\MySql\Impl\Traits\CmdTraits\TWithWhere;
	use \Squid\MySql\Impl\Traits\CmdTraits\TWithLimit;
	use \Squid\MySql\Impl\Traits\CmdTraits\TWithColumn;
	use \Squid\MySql\Impl\Traits\CmdTraits\TWithExtendedWhere;

	const PART_DISTINCT		= 0;
	const PART_COLUMNS		= 1;
	const PART_FROM			= 2;
	const PART_WHERE		= 3;
	const PART_GROUP_BY		= 4;
	const PART_WITH_ROLL_UP	= 5;
	const PART_HAVING		= 6;
	const PART_ORDER_BY		= 7;
	const PART_LIMIT		= 8;
	const PART_UNION		= 9;
	const PART_LOCK			= 10;

	/**
	 * @var array Collection of default values.
	 */
	private static $DEFAULT = array(
		CmdSelect::PART_DISTINCT		=> false,
		CmdSelect::PART_COLUMNS			=> false,
		CmdSelect::PART_FROM			=> false,
		CmdSelect::PART_WHERE			=> false,
		CmdSelect::PART_GROUP_BY		=> false,
		CmdSelect::PART_WITH_ROLL_UP	=> false,
		CmdSelect::PART_HAVING			=> false,
		CmdSelect::PART_ORDER_BY		=> false,
		CmdSelect::PART_LIMIT			=> false,
		CmdSelect::PART_UNION			=> false,
		CmdSelect::PART_LOCK			=> false
	);

	/**
	 * @param IMySqlCommandConstructor $from
	 * @param string $alias
	 * @param string $condition
	 * @param string $join
	 * @return static
	 */
	private function fromSubQuery(IMySqlCommandConstructor $from, $alias, $condition = '', $join = '')
	{
		if ($join) $join .= ' ';

		$cmd = '';

		if ($alias)
			$cmd .= " $alias";

		if ($condition)
			$cmd .= " ON $condition";

		$sql = $from->assemble();

		return $this->appendPart(
			CmdSelect::PART_FROM,
			"$join($sql)$cmd",
			$from->bind()
		);
	}

	/**
	 * @param string|IMySqlCommandConstructor $joinWith
	 * @param string $alias
	 * @param string $condition
	 * @param array|bool $bind
	 * @param string $join
	 * @return static
	 */
	private function joinWith($joinWith, $alias, $condition, $bind, $join)
	{
		if ($joinWith instanceof IMySqlCommandConstructor)
			return $this->fromSubQuery($joinWith, $alias, $condition, $join);

		return $this->appendPart(
			CmdSelect::PART_FROM,
			"$join $joinWith $alias ON $condition",
			$bind
		);
	}

	/**
	 * @see \Squid\MySql\Impl\Traits\CmdTraits\TWithColumn
	 * @param string[] $columns
	 * @param array|false $bind
	 * @return static
	 */
	private function addColumn(array $columns, $bind)
	{
		$this->appendPart(CmdSelect::PART_COLUMNS, $columns, $bind);
		return $this;
	}

	/**
	 * Get the parts this query can have.
	 * @return array Array containing only the part as keys and values set to false.
	 */
	protected function getDefaultParts()
	{
		return CmdSelect::$DEFAULT;
	}

	/**
	 * Combine all the parts into one sql.
	 * @return string Sql query
	 */
	protected function generate()
	{
		$command =
			'SELECT ' .
				($this->getPart(CmdSelect::PART_DISTINCT) ? 'DISTINCT ' : '').
				Assembly::appendDirectly(($this->getPart(CmdSelect::PART_COLUMNS) ?: array('*')), ',');

		$union = $this->getPart(CmdSelect::PART_UNION);
		$from = $this->getPart(CmdSelect::PART_FROM);

		if (!$from && !$union)
		{
			return $command;
		}

		$lock = $this->getPart(CmdSelect::PART_LOCK);

		$command .=
			Assembly::append('FROM', $from, ' ') .
			Assembly::appendWhere($this->getPart(CmdSelect::PART_WHERE)) .
			Assembly::append('GROUP BY', $this->getPart(CmdSelect::PART_GROUP_BY)) .
			($this->getPart(CmdSelect::PART_WITH_ROLL_UP) ? 'WITH ROLLUP ' : '') .
			Assembly::append('HAVING', $this->getPart(CmdSelect::PART_HAVING)) .
			Assembly::append('ORDER BY', $this->getPart(CmdSelect::PART_ORDER_BY)) .
			Assembly::append('LIMIT', $this->getPart(CmdSelect::PART_LIMIT));

		if ($union)
		{
			$union = implode(' ', $union);
			$command = "($command) $union";
		}

		return $command . ($lock ? "$lock " : '');
	}

	/**
	 * @param bool $distinct
	 * @return static
	 */
	public function distinct($distinct = true)
	{
		return $this->setPart(CmdSelect::PART_DISTINCT, $distinct);
	}

	/**
	 * @param string|IMySqlCommandConstructor $table
	 * @param bool|string $alias
	 * @return static
	 */
	public function from($table, $alias = false)
	{
		if ($table instanceof IMySqlCommandConstructor)
		{
			$this->setPart(CmdSelect::PART_FROM, false);
			return $this->fromSubQuery($table, $alias);
		}

		if ($alias)
		{
			$table = "$table $alias";
		}

		return $this->setPart(CmdSelect::PART_FROM, [$table]);
	}

	/**
	 * @param string|IMySqlCommandConstructor $table
	 * @param string $alias
	 * @param string $condition
	 * @param mixed|array $bind
	 * @return static
	 */
	public function join($table, $alias, $condition, $bind = false)
	{
		return $this->joinWith($table, $alias, $condition, $bind, 'JOIN');
	}

	/**
	 * @param string|IMySqlCommandConstructor $table
	 * @param string $alias
	 * @param string $condition
	 * @param mixed|array $bind
	 * @param bool $outer
	 * @return static
	 */
	public function leftJoin($table, $alias, $condition, $bind = false, $outer = false)
	{
		return $this->joinWith(
			$table, $alias, $condition, $bind,
			($outer ? 'LEFT OUTER JOIN' : 'LEFT JOIN'));
	}

	/**
	 * @param string|IMySqlCommandConstructor $table
	 * @param string $alias
	 * @param string $condition
	 * @param mixed|array $bind
	 * @param bool $outer
	 * @return static
	 */
	public function rightJoin($table, $alias, $condition, $bind = false, $outer = false)
	{
		return $this->joinWith(
			$table, $alias, $condition, $bind,
			($outer ? 'RIGHT OUTER JOIN' : 'RIGHT JOIN'));
	}

	/**
	 * @param string|array $column
	 * @param array|bool $bind
	 * @return static
	 */
	public function groupBy($column, $bind = false)
	{
		if (is_array($column)) $column = implode(',', $column);

		return $this->appendPart(CmdSelect::PART_GROUP_BY, $column, $bind);
	}

	/**
	 * @param string $exp
	 * @param mixed|array|bool $bind
	 * @return static
	 */
	public function having($exp, $bind = false)
	{
		return $this->appendPart(CmdSelect::PART_HAVING, $exp, $bind);
	}

	/**
	 * @param array $columns
	 * @return mixed
	 */
	public function _orderBy(array $columns)
	{
		return $this->appendPart(CmdSelect::PART_ORDER_BY, $columns);
	}

	/**
	 * @param bool $withRollup
	 * @return static
	 */
	public function withRollup($withRollup = true)
	{
		return $this->setPart(CmdSelect::PART_WITH_ROLL_UP, $withRollup);
	}

	/**
	 * @param ICmdSelect $select
	 * @param bool $all
	 * @return static
	 */
	public function union(IMySqlCommandConstructor $select, $all = false)
	{
		$union = 'UNION ' . ($all ? 'ALL ' : '');

		return $this->appendPart(
			CmdSelect::PART_UNION,
			$union . "({$select->assemble()})",
			$select->bind());
	}

	/**
	 * @param ICmdSelect $select
	 * @return static
	 */
	public function unionAll(IMySqlCommandConstructor $select)
	{
		return $this->union($select, true);
	}

	/**
	 * @param bool $forUpdate
	 * @return static
	 */
	public function forUpdate($forUpdate = true)
	{
		return $this->setPart(CmdSelect::PART_LOCK,
			($forUpdate ? 'FOR UPDATE' : false));
	}

	/**
	 * @param bool $lockInShareMode
	 * @return static
	 */
	public function lockInShareMode($lockInShareMode = true)
	{
		return $this->setPart(CmdSelect::PART_LOCK,
			($lockInShareMode ? 'LOCK IN SHARE MODE' : false));
	}

	/**
	 * @param string $exp
	 * @param mixed|array|null $bind
	 * @return static
	 */
	public function where($exp, $bind = false)
	{
		return $this->appendPart(CmdSelect::PART_WHERE, $exp, $bind);
	}

	/**
	 * @param int $from
	 * @param int $count
	 * @return IWithLimit|static
	 */
	public function limit($from, $count): IWithLimit
	{
		return $this->setPart(CmdSelect::PART_LIMIT, [(int)$from, (int)$count], []);
	}

	/**
	 * @return bool|null
	 */
	public function queryExists()
	{
		$cmdExists = new CmdSelect();
		$cmdExists->setConnection($this->getConn());
		$cmdExists->setPart(
			CmdSelect::PART_COLUMNS,
			['EXISTS (' . $this->assemble() . ')'],
			$this->bind());

		$result = $cmdExists->queryScalar(null);

		return (is_null($result) ? null : (bool)$result);
	}

	/**
	 * @return int|bool
	 */
	public function queryCount()
	{
		$union		= $this->getPart(CmdSelect::PART_UNION);
		$distinct	= $this->getPart(CmdSelect::PART_DISTINCT);

		if ($union || $distinct)
		{
			$countSelect = new CmdSelect();
			$countSelect->setConnection($this->getConn());
			$countSelect->columnsExp('COUNT(*)')->from($this, 'a');
			return $countSelect->queryInt();
		}

		$select = clone $this;

		$groupBy		= $this->getPart(CmdSelect::PART_GROUP_BY);
		$groupByBinds	= $this->getBind(CmdSelect::PART_GROUP_BY);

		if ($groupBy)
		{
			$groupByQuery = implode(',', $groupBy);

			$select->setPart(CmdSelect::PART_COLUMNS, array("COUNT(DISTINCT $groupByQuery)"), $groupByBinds);
			$select->setPart(CmdSelect::PART_GROUP_BY, false);
		}
		else
		{
			$select->setPart(CmdSelect::PART_COLUMNS, array('COUNT(*)'));
		}

		$select->distinct(false);
		$select->forUpdate(false);
		$select->lockInShareMode(false);
		$select->setPart(CmdSelect::PART_ORDER_BY, '', false);

		return $select->queryInt();
	}

	public function __clone() {}
}


<?php
namespace Squid;

use Squid\MySql\ConfigFacade;
use Squid\MySql\IMySqlConnector;
use Squid\MySql\Connection\IMySqlConnection;

use Squid\MySql\Impl\MySqlConnector;
use Squid\MySql\Impl\Connection\ConnectionBuilder;

class MySql
{
	/** @var ConfigFacade */
	private $configFacade;

	/** @var MySqlConnector[] */
	private $sharedConnectors;

	/** @var ConnectionBuilder */
	private $connectionBuilder = null;

	public function __construct()
	{
		$this->configFacade = new ConfigFacade();
	}

	/**
	 * @param string $name
	 * @return IMySqlConnection
	 */
	private function getNewConnection($name)
	{
		$config = $this->configFacade->getConfig($name);

		if (!$this->connectionBuilder)
		{
			$this->connectionBuilder = new ConnectionBuilder();
			$this->connectionBuilder->setDecorators($this->configFacade->getDecorators());
		}

		return $this->connectionBuilder->create($config);
	}

	/**
	 * @return ConfigFacade
	 */
	public function config()
	{
		return $this->configFacade;
	}

	/**
	 * @param string $name
	 * @return IMySqlConnector
	 */
	public function getConnector($name = 'main')
	{
		if (!isset($this->sharedConnectors[$name]))
		{
			$this->sharedConnectors[$name] = $this->createConnector($name);
		}

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

	/**
	 * Always return a connector using a new connection.
	 * Note that closeAll will not affect connectors returned by this method.
	 * @param string $name
	 * @return IMySqlConnector
	 */
	public function createConnector($name)
	{
		$connector = new MySqlConnector($name);
		$connector->setConnection($this->getNewConnection($name));
		return $connector;
	}

	public function closeAll()
	{
		foreach ($this->sharedConnectors as $connector)
		{
			$connector->close();
		}
	}

	public static function staticConnector(array $config)
	{
		$mysql = new MySql();
		$mysql->config()->addConfig('main', $config);
		return $mysql->getConnector('main');
	}
}


<?php
namespace Squid\MySql\Impl\Traits\CmdTraits;

use Squid\MySql\Command\IWithLimit;
use Squid\OrderBy;

/**
 * Implements calculation behavior for the IWithLimit interface. Relies on the using class
 * to implement methods limit($from, $count), and _orderBy(array $expressions).
 *
 * @method mixed limit(int $from, int $count)
 * @method mixed _orderBy(array $expressions)
 * @mixin \Squid\MySql\Command\IWithLimit
 */
trait TWithLimit
{
	/**
	 * Append the DESC keyword to all requested keywords or expressions.
	 * @param string|array $column
	 */
	private function appendDesc(&$column): void
	{
		if (is_array($column))
		{
			foreach ($column as &$col)
			{
				$col = "$col DESC";
			}
		}
		else
		{
			$column = ["$column DESC"];
		}
	}

	/**
	 * Set as limit but with $from always equals to zero.
	 * @param int $count Maximum number of rows to select.
	 * @return IWithLimit|static
	 */
	public function limitBy($count) : IWithLimit
	{
		return $this->limit(0, $count);
	}

	/**
	 * Use limit statement for a page expression.
	 * @param int $page Zero based index of the page to select.
	 * @param int $pageSize Number of elements per page.
	 * @return IWithLimit|static
	 */
	public function page($page, $pageSize): IWithLimit
	{
		return $this->limit($page * $pageSize, $pageSize);
	}

	/**
	 * Add order by fields.
	 * @param string|array $column Single column, expression or array of columns.
	 * @param int $type Order type. Use OrderBy consts. Either single value, or array of
	 * values. In the later, $column must be of same size.
	 * @return IWithLimit|static
	 */
	public function orderBy($column, $type = OrderBy::ASC): IWithLimit
	{
		if ($type == OrderBy::DESC)
		{
			$this->appendDesc($column);
		}
		else if (!is_array($column))
		{
			$column = [$column];
		}

		return $this->_orderBy($column);
	}
}


<?php
namespace Squid\MySql\Impl\Command;

abstract class PartsCommand extends AbstractCommand
{
	/** @var array */
	private $parts	= [];

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

	/**
	 * Append bind values to given part.
	 * @param int $part Part to append to.
	 * @param mixed|array|false $bind
	 * @return static
	 */
	private function appendBind($part, $bind)
	{
		if ($bind === false) return $this;

		if (!is_array($bind)) $bind = [$bind];

		if (!$this->bind[$part])
		{
			$this->bind[$part] = $bind;
		}
		else
		{
			$this->bind[$part] = array_merge($this->bind[$part], $bind);
		}

		return $this;
	}

	/**
	 * @param array $finalParams
	 * @param array $singlePartParams
	 * @return array
	 */
	private function combineBindParams($finalParams, $singlePartParams)
	{
		foreach ($singlePartParams as &$param)
		{
			if ($param instanceof \DateTime)
			{
				$param = $param->format('Y-m-d H:i:s');
			}
		}

		return array_merge($finalParams, $singlePartParams);
	}

	/**
	 * Append new query to given part.
	 * @param int $part Part to append to.
	 * @param string $sql Command to append.
	 * @param array|bool $bind Bind params.
	 * @return static
	 */
	protected function appendPart($part, $sql, $bind = false)
	{
		if (!is_array($sql)) $sql = [$sql];

		if (!$this->parts[$part])
		{
			$this->parts[$part] = $sql;
		}
		else
		{
			$this->parts[$part] = array_merge($this->parts[$part], $sql);
		}

		return $this->appendBind($part, $bind);
	}

	/**
	 * Override part value.
	 * @param int $part Part to set.
	 * @param string|array $sql Value to set for this part. Use only for parts that will not
	 * be appended to. Otherwise pass as array.
	 * @param array|bool $bind Array of bind values if any.
	 * @return static
	 */
	protected function setPart($part, $sql, $bind = false)
	{
		$this->parts[$part] = $sql;

		if (is_array($bind)) 		$this->bind[$part] = $bind;
		else if ($bind === false)	$this->bind[$part] = false;
		else						$this->bind[$part] = [$bind];

		return $this;
	}

	/**
	 * @param int $part Part to return.
	 * @return mixed Value stored in this part.
	 */
	protected function getPart($part)
	{
		return $this->parts[$part];
	}

	/**
	 * @param int $part Part to the the bind values for.
	 * @return mixed
	 */
	protected function getBind($part)
	{
		return $this->bind[$part];
	}

	/**
	 * Get the parts this query can have.
	 * @return array Array containing only the part as keys and values set to false.
	 */
	protected abstract function getDefaultParts();

	/**
	 * Combine all the parts into one sql.
	 * @return string Sql query
	 */
	protected abstract function generate();

	public function __construct()
	{
		$this->parts= $this->getDefaultParts();
		$this->bind	= $this->parts;
	}

	/**
	 * @return array
	 */
	public function bind()
	{
		$bindParams = [];

		foreach ($this->bind as $partParams)
		{
			if ($partParams)
			{
				$bindParams = $this->combineBindParams($bindParams, $partParams);
			}
		}

		return $bindParams;
	}

	/**
	 * @return string
	 */
	public function assemble()
	{
		return $this->generate();
	}
}


<?php

namespace Clue\QDataStream;

// http://doc.qt.io/qt-4.8/qdatastream.html#details
class Writer
{
    private $userTypeMap;
    private $hasNull = true;

    private $buffer = '';
    private $supportsExtMbstring;

    /**
     * @param array $userTypeMap
     */
    public function __construct($userTypeMap = array())
    {
        $this->userTypeMap = $userTypeMap;

        // prefer mb_convert_encoding if available
        $this->supportsExtMbstring = \function_exists('mb_convert_encoding');
    }

    /**
     * Returns concatenated write buffer as a single string
     *
     * @return string
     */
    public function __toString()
    {
        return $this->buffer;
    }

    /**
     * @param int $int INT32
     * @return void
     */
    public function writeInt($int)
    {
        $this->writeBE(pack('l', $int));
    }

    /**
     * @param int $int UINT32
     * @return void
     */
    public function writeUInt($int)
    {
        $this->buffer .= pack('N', $int);
    }

    /**
     * @param int $int INT16
     * @return void
     */
    public function writeShort($int)
    {
        $this->writeBE(pack('s', $int));
    }

    /**
     * @param int $int UINT16
     * @return void
     */
    public function writeUShort($int)
    {
        $this->buffer .= pack('n', $int);
    }

    /**
     * @param int $int INT8
     * @return void
     */
    public function writeChar($int)
    {
        $this->buffer .= pack('c', $int);
    }

    /**
     * @param int $int UINT8
     * @return void
     */
    public function writeUChar($int)
    {
        $this->buffer .= pack('C', $int);
    }

    /**
     * @param string[] $strings array of text strings in UTF-8 encoding (will be transcoded to UTF-16BE)
     * @return void
     */
    public function writeQStringList(array $strings)
    {
        $this->writeUInt(count($strings));

        foreach ($strings as $string) {
            $this->writeQString($string);
        }
    }

    /**
     * @param string|null $str text string in UTF-8 encoding (will be transcoded to UTF-16BE)
     * @return void
     * @see self::writeQByteArray() for writing binary data
     */
    public function writeQString($str)
    {
        if ($str !== null && $str !== '') {
            $str = $this->conv($str);
        }

        $this->writeQByteArray($str);
    }

    /**
     * @param string $char single text character in UTF-8 encoding. This will be
     *     transcoded to UTF-16BE (or actually UCS-2BE because it only supports
     *     BMP characters) and uses "?" replacement character otherwise.
     * @return void
     */
    public function writeQChar($char)
    {
        // transcode to UTF-16BE and assume result to be exactly two bytes, otherwise use "?"
        $char = $this->conv($char);
        $this->buffer .= isset($char[1]) && !isset($char[2]) ? $char : "\x00?";
    }

    /**
     * @param string|null $bytes binary byte string
     * @return void
     * @see self::writeQString() for writing text string
     */
    public function writeQByteArray($bytes)
    {
        if ($bytes === null) {
            $this->buffer .= "\xFF\xFF\xFF\xFF";
        } else {
            $this->writeUInt(strlen($bytes));
            $this->buffer .= $bytes;
        }
    }

    /**
     * @param bool $value
     * @return void
     */
    public function writeBool($value)
    {
        // http://docs.oracle.com/javase/7/docs/api/java/io/DataOutput.html#writeBoolean%28boolean%29
        $this->buffer .= $value ? "\x01" : "\x00";
    }

    /**
     * @param QVariant|mixed $value
     * @return void
     * @throws \UnexpectedValueException if an unknown QUserType is encountered
     */
    public function writeQVariant($value)
    {
        if ($value instanceof QVariant) {
            $type = $value->getType();
            $value = $value->getValue();
        } else {
            $type = Types::getTypeByValue($value);
        }

        if (is_string($type)) {
            $this->writeUInt(Types::TYPE_QUSER_TYPE);
            if ($this->hasNull) {
                $this->buffer .= "\x00";
            }

            return $this->writeQUserTypeByName($value, $type);
        }

        $name = 'write' . Types::getNameByType($type);
        if (!method_exists($this, $name)) {
            throw new \BadMethodCallException('Known variant type (' . $type . '), but has no "' . $name . '()" method'); // @codeCoverageIgnore
        }

        $this->writeUInt($type);
        if ($this->hasNull) {
            $this->buffer .= "\x00";
        }
        $this->$name($value);
    }

    /**
     * @param mixed  $value
     * @param string $userType
     * @return void
     * @throws \UnexpectedValueException if an unknown QUserType is encountered
     */
    public function writeQUserTypeByName($value, $userType)
    {
        if (!isset($this->userTypeMap[$userType])) {
            throw new \UnexpectedValueException('Unknown user type "' . $userType . '" does not have any data mapping');
        }
        $this->writeQByteArray($userType . "\x00");

        $fn = $this->userTypeMap[$userType];
        $fn($value, $this);
    }

    /**
     * @param array<QVariant|mixed> $list
     * @return void
     * @throws \UnexpectedValueException if an unknown QUserType is encountered
     */
    public function writeQVariantList(array $list)
    {
        $this->writeUInt(count($list));

        foreach ($list as $value) {
            $this->writeQVariant($value);
        }
    }

    /**
     * @param array|\stdClass $map
     * @return void
     * @throws \UnexpectedValueException if an unknown QUserType is encountered
     */
    public function writeQVariantMap($map)
    {
        if ($map instanceof \stdClass) {
            $map = (array)$map;
        }

        $this->writeUInt(count($map));

        foreach ($map as $key => $value) {
            $this->writeQString($key);
            $this->writeQVariant($value);
        }
    }

    /**
     * Writes a QTime for the given timestamp or DateTime object
     *
     * The QTime will only carry the number of milliseconds since midnight.
     * This means you should probably only use this for times within the current
     * day.
     *
     * If you pass a timestamp from any other day, it will write the number of
     * milliseconds that passed since that day's midnight. Note that reading
     * this number has no indication this is not the current day, so you're
     * likely going to lose the day information and may end up with wrong dates.
     *
     * The QTime will be sent as the number of milliseconds since midnight,
     * without any awareness of timezone or DST properties. Thus, writing this
     * will assume it is relative to the current timezone. This means that the
     * time "14:10:34.5108" will be 14h after midnight, irrespective of its
     * actual timezone. The receiving side may not be aware of your local
     * timezone, so it can only assume its own local timezone as a base.
     *
     * Make sure to use (i.e. convert via `setTimeZone()`) to the same timezone
     * on both sides or consider using the `writeQDateTime()` method instead,
     * which uses absolute time stamps and does not suffer from this.
     *
     * You can also pass a Unix timestamp to this function, this will be assumed
     * to be relative to the local midnight timestamp. If you need more control
     * over your timezone, consider passing a `DateTime` object instead.
     *
     * @param \DateTime|float $timestamp
     * @see self::writeQDateTime
     */
    public function writeQTime($timestamp)
    {
        if ($timestamp instanceof \DateTime) {
            $msec = $timestamp->format('H') * 3600000 +
                    $timestamp->format('i') * 60000 +
                    $timestamp->format('s') * 1000 +
                    (int)($timestamp->format('0.u') * 1000);
        } else {
            $msec = round(($timestamp - strtotime('midnight', (int)$timestamp)) * 1000);
        }

        $this->writeUInt($msec);
    }

    /**
     * @param \DateTime|float $timestamp
     * @return void
     */
    public function writeQDateTime($timestamp)
    {
        if ($timestamp instanceof \DateTime) {
            $timestamp = $timestamp->format('U.u');
        }

        // days:uint, seconds:uint, isUTC:uchar
        $this->buffer .= pack(
            'NNC',
            floor($timestamp / 86400) + 2440588,
            round(($timestamp - floor($timestamp / 86400) * 86400) * 1000),
            1
        );
    }

    /**
     * transcode UTF-8 to UTF-16BE
     *
     * @param string $str
     * @return string
     */
    private function conv($str)
    {
        // prefer mb_convert_encoding if available
        if ($this->supportsExtMbstring) {
            return mb_convert_encoding($str, 'UTF-16BE', 'UTF-8');
        }

        // otherwise match each UTF-8 character byte sequence, manually convert
        // it to its Unicode code point and then encode as UTF-16BE byte sequence.
        return preg_replace_callback('/./su', function ($m) {
            if (!isset($m[0][1])) {
                // U+0000 - U+007F single byte ASCII/UTF-8 character
                return "\x00" . $m[0];
            }

            if (isset($m[0][3])) {
                // U+10000 - U+10FFFF uses four UTF-8 bytes and 4 UTF-16 bytes
                // get code point and convert to higher and lower surrogate
                $code = ((ord($m[0][0]) & 0x07) << 18) + ((ord($m[0][1]) & 0x3F) << 12) + ((ord($m[0][2]) & 0x3F) << 6) + (ord($m[0][3]) & 0x3F);
                return pack('nn', ($code - 0x10000 >> 10) | 0xD800, ($code & 0x03FF) | 0xDC00);
            }

            if (isset($m[0][2])) {
                // U+0800 - U+FFFF uses three UTF-8 bytes
                $code = ((ord($m[0][0]) & 0x0F) << 12) + ((ord($m[0][1]) & 0x3F) << 6) + (ord($m[0][2]) & 0x3F);
            } else {
                // U+0080 - U+07FF uses two UTF-8 bytes
                $code = ((ord($m[0][0]) & 0x1F) << 6) + (ord($m[0][1]) & 0x3F);
            }

            return chr($code >> 8) . chr($code & 0xFF);
        }, $str);
    }

    private function writeBE($bytes)
    {
        // check if machine byte order is already BE, otherwise reverse LE to BE
        if (pack('S', 1) !== "\x00\x01") {
            $bytes = strrev($bytes);
        }

        $this->buffer .= $bytes;
    }
}


<?php declare(strict_types=1);

namespace One;

use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Psr7\MultipartStream;
use One\Model\Article;
use One\Model\Model;
use Psr\Http\Message\RequestInterface;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;

/**
 * Publisher class
 * main class to be used that interfacing to the API
 */
class Publisher implements LoggerAwareInterface
{
    public const DEFAULT_MAX_ATTEMPT = 4;

    public const REST_SERVER = 'https://www.one.co.id';

    public const AUTHENTICATION = '/oauth/token';

    public const ARTICLE_CHECK_ENDPOINT = '/api/article';

    public const ARTICLE_ENDPOINT = '/api/publisher/article';

    /**
     * attachment url destination
     * @var array<string>
     */
    private $attachmentUrl;

    /**
     * Logger variable, if set log activity to this obejct each time sending request and receiving response
     *
     * @var \Psr\Log\LoggerInterface
     */
    private $logger = null;

    /**
     * credentials props
     *
     * @var string
     */
    private $clientId;

    /**
     * client secret
     * @var string
     */
    private $clientSecret;

    /**
     * Oauth access token response
     *
     * @var string
     */
    private $accessToken = null;

    /**
     * publisher custom options
     *
     * @var \One\Collection
     */
    private $options;

    /**
     * http transaction Client
     *
     * @var \GuzzleHttp\Client;
     */
    private $httpClient;

    /**
     * token saver storage
     *
     * @var \Closure
     */
    private $tokenSaver = null;

    /**
     * constructor
     */
    public function __construct(string $clientId, string $clientSecret, array $options = [])
    {
        $this->clientId = $clientId;
        $this->clientSecret = $clientSecret;

        $this->assessOptions($options);

        $this->attachmentUrl = [
            Article::ATTACHMENT_FIELD_GALLERY => self::ARTICLE_ENDPOINT . '/{article_id}/gallery',
            Article::ATTACHMENT_FIELD_PAGE => self::ARTICLE_ENDPOINT . '/{article_id}/page',
            Article::ATTACHMENT_FIELD_PHOTO => self::ARTICLE_ENDPOINT . '/{article_id}/photo',
            Article::ATTACHMENT_FIELD_VIDEO => self::ARTICLE_ENDPOINT . '/{article_id}/video',

        ];
    }

    /**
     * recycleToken from callback. If use external token storage could leveraged on this
     */
    public function recycleToken(\Closure $tokenProducer): self
    {
        return $this->setAuthorizationHeader(
            $tokenProducer()
        );
    }

    /**
     * set Token Saver
     */
    public function setTokenSaver(\Closure $tokenSaver): self
    {
        $this->tokenSaver = $tokenSaver;

        return $this;
    }

    public function getTokenSaver(): \Closure
    {
        return $this->tokenSaver;
    }

    /**
     * submitting article here, return new Object cloned from original
     */
    public function submitArticle(Article $article): \One\Model\Article
    {
        $responseArticle = $this->post(
            self::ARTICLE_ENDPOINT,
            $this->normalizePayload(
                $article->getCollection()
            )
        );

        $responseArticle = json_decode($responseArticle, true);
        $article->setId((string) $responseArticle['data']['id']);

        foreach ($article->getPossibleAttachment() as $field) {
            if ($article->hasAttachment($field)) {
                foreach ($article->getAttachmentByField($field) as $attachment) {
                    $this->submitAttachment(
                        $article->getId(),
                        $attachment,
                        $field
                    );
                }
            }
        }

        return $article;
    }

    /**
     * submit each attachment of an article here
     */
    public function submitAttachment(string $idArticle, Model $attachment, string $field): array
    {
        return json_decode(
            $this->post(
                $this->getAttachmentEndPoint($idArticle, $field),
                $this->normalizePayload(
                    $attachment->getCollection()
                )
            ),
            true
        );
    }

    /**
     * get article from rest API
     *
     * @return string json
     */
    public function getArticle(string $idArticle): string
    {
        return $this->get(
            self::ARTICLE_CHECK_ENDPOINT . "/${idArticle}"
        );
    }

    /**
     * get list article by publisher
     *
     * @return string json
     */
    public function listArticle(): string
    {
        return $this->get(
            self::ARTICLE_ENDPOINT
        );
    }

    /**
     * delete article based on id
     */
    public function deleteArticle(string $idArticle): ?string
    {
        $articleOnRest = $this->getArticle($idArticle);

        if (! empty($articleOnRest)) {
            $articleOnRest = json_decode($articleOnRest, true);

            if (isset($articleOnRest['data'])) {
                foreach (Article::getDeleteableAttachment() as $field) {
                    if (isset($articleOnRest['data'][$field])) {
                        foreach ($articleOnRest['data'][$field] as $attachment) {
                            if (isset($attachment[$field . '_order'])) {
                                $this->deleteAttachment($idArticle, $field, $attachment[$field . '_order']);
                            }
                        }
                    }
                }
            }

            return $this->delete(
                $this->getArticleWithIdEndPoint($idArticle)
            );
        }
    }

    /**
     * delete attachment of an article
     */
    public function deleteAttachment(string $idArticle, string $field, string $order): string
    {
        return $this->delete(
            $this->getAttachmentEndPoint($idArticle, $field) . "/${order}"
        );
    }

    /**
     * get proxy
     */
    final public function get(string $path, array $header = [], array $options = []): string
    {
        return $this->requestGate(
            'GET',
            $path,
            $header,
            [],
            $options
        );
    }

    /**
     * post proxy
     *
     * @param \One\Collection|array $body
     */
    final public function post(string $path, array $body = [], array $header = [], array $options = []): string
    {
        if ($this->hasLogger()) {
            $this->logger->info('Post to ' . $path);
        }

        return $this->requestGate(
            'POST',
            $path,
            $header,
            $body,
            $options
        );
    }

    /**
     * delete proxy
     *
     * @param \One\Collection|array $body
     * @param array $body
     */
    final public function delete(string $path, array $body = [], array $header = [], array $options = []): string
    {
        return $this->requestGate(
            'DELETE',
            $path,
            $header,
            $body,
            $options
        );
    }

    /**
     * @inheritDoc
     */
    public function setLogger(LoggerInterface $logger): void
    {
        $this->logger = $logger;
    }

    public function getRestServer(): string
    {
        return $this->options->get('rest_server');
    }

    /**
     * assessing and custom option
     */
    private function assessOptions(array $options): void
    {
        $defaultOptions = [
            'rest_server' => self::REST_SERVER,
            'auth_url' => self::AUTHENTICATION,
            'max_attempt' => self::DEFAULT_MAX_ATTEMPT,
            'default_headers' => [
                'Accept' => 'application/json',
            ],
        ];

        $this->options = new Collection(
            array_merge(
                $defaultOptions,
                $options
            )
        );

        if (isset($options['access_token'])) {
            $this->setAuthorizationHeader($options['access_token']);
        }

        if (isset($options['recycle_token']) && is_callable($options['recycle_token'])) {
            $this->recycleToken(
                $options['recycle_token']
            );
        }

        if (isset($options['token_saver']) && is_callable($options['token_saver'])) {
            $this->setTokenSaver(
                $options['token_saver']
            );
        }

        $this->httpClient = new Client([
            'base_uri' => $this->options->get('rest_server'),
        ]);
    }

    /**
     * one gate menu for request creation.
     *
     * @param \One\Collection|array $body
     */
    private function requestGate(string $method, string $path, array $header = [], array $body = [], array $options = []): string
    {
        if (empty($this->accessToken)) {
            $this->renewAuthToken();
        }

        $request = new \GuzzleHttp\Psr7\Request(
            $method,
            $path,
            array_merge(
                $this->options->get('default_headers'),
                $header
            ),
            $this->createBodyForRequest(
                $this->prepareMultipartData($body)
            )
        );

        return (string) $this->sendRequest($request);
    }

    private function prepareMultipartData(array $data = []): array
    {
        $result = [];
        foreach ($data as $key => $value) {
            array_push($result, ['name' => $key, 'contents' => $value]);
        }
        return $result;
    }

    /**
     * actually send request created here, separated for easier attempt count and handling exception
     *
     * @throws \Exception
     * @throws \GuzzleHttp\Exception\ClientException
     * @throws \GuzzleHttp\Exception\BadResponseException
     */
    private function sendRequest(RequestInterface $request, int $attempt = 0): \Psr\Http\Message\StreamInterface
    {
        if ($attempt >= $this->options->get('max_attempt')) {
            throw new \Exception('MAX attempt reached for ' . $request->getUri() . ' with payload ' . (string) $request);
        }

        try {
            $response = $this->httpClient->send(
                $request,
                [
                    'allow_redirects' => false,
                    'synchronous' => true,
                    'curl' => [
                        CURLOPT_FORBID_REUSE => true,
                        CURLOPT_MAXCONNECTS => 30,
                        CURLOPT_SSL_VERIFYPEER => false,
                        CURLOPT_SSL_VERIFYSTATUS => false,
                    ],
                ]
            );
            if ($response->getStatusCode() === 200) {
                return $response->getBody();
            }

            return $this->sendRequest($request, $attempt++);
        } catch (ClientException $err) {
            if ($err->getResponse()->getStatusCode() === 401 && $request->getRequestTarget() !== self::AUTHENTICATION) {
                $this->renewAuthToken();
                return $this->sendRequest($request, $attempt++);
            }

            throw $err;
        } catch (\Throwable $err) {
            throw $err;
        }
    }

    /**
     * createBodyForRequest
     */
    private function createBodyForRequest(array $body = []): ?\GuzzleHttp\Psr7\MultiPartStream
    {
        if (empty($body)) {
            return null;
        }
        return new MultipartStream($body);
    }

    /**
     * renewing access_token
     *
     * @throws \Exception
     */
    private function renewAuthToken(): self
    {
        $request = new \GuzzleHttp\Psr7\Request(
            'POST',
            self::AUTHENTICATION,
            $this->options->get('default_headers'),
            $this->createBodyForRequest([
                ['name' => 'grant_type',
                    'contents' => 'client_credentials', ],
                ['name' => 'client_id',
                    'contents' => $this->clientId, ],
                ['name' => 'client_secret',
                    'contents' => $this->clientSecret, ],
            ])
        );

        $token = (string) $this->sendRequest($request);

        $token = json_decode($token, true);

        if (empty($token)) {
            throw new \Exception('Access token request return empty response');
        }

        if (! empty($this->tokenSaver)) {
            $this->getTokenSaver()(
                $token['access_token']
            );
        }

        return $this->setAuthorizationHeader(
            $token['access_token']
        );
    }

    /**
     * set header for OAuth 2.0
     */
    private function setAuthorizationHeader(string $accessToken): self
    {
        $this->accessToken = $accessToken;

        $this->options->set(
            'default_headers',
            array_merge(
                $this->options->get('default_headers'),
                [
                    'Authorization' => 'Bearer ' . $accessToken,
                ]
            )
        );

        return $this;
    }

    /**
     * get Attachment Submission url Endpoint at rest API
     */
    private function getAttachmentEndPoint(string $idArticle, string $field): string
    {
        return $this->replaceEndPointId(
            $idArticle,
            $this->attachmentUrl[$field]
        );
    }

    /**
     * get article endpoint for deleting api
     */
    private function getArticleWithIdEndPoint(string $identifier): string
    {
        return self::ARTICLE_ENDPOINT . "/${identifier}";
    }

    /**
     * function that actually replace article_id inside endpoint pattern
     */
    private function replaceEndPointId(string $identifier, string $url): string
    {
        return str_replace(
            '{article_id}',
            $identifier,
            $url
        );
    }

    /**
     * normalizing payload. not yet implemented totally, currently just bypass a toArray() function from collection.
     */
    private function normalizePayload(Collection $collection): array
    {
        return $collection->toArray();
    }

    /**
     * Checks if Logger instance exists
     */
    private function hasLogger(): bool
    {
        return isset($this->logger) && $this->logger !== null;
    }
}


<?php

/**
 * This file is part of the Mordilion\Configurable package.
 *
 * For the full copyright and license information, please view the
 * LICENSE file that was distributed with this source code.
 *
 * @copyright (c) Henning Huncke - <mordilion@gmx.de>
 */

namespace Mordilion\Configurable\Configuration\Reader\Decoder;

use Mordilion\Configurable\Configuration\Reader\Decoder\DecoderInterface;

/**
 * Mordilion\Configurable Decoder class.
 *
 * @author Frederik Wauters <frederik.wauters@web.de>
 */
class SimpleXml implements DecoderInterface
{
    /**
     * Simple XML decoder
     *
     * @param  string $xml
     *
     * @return array
     */
    public function decode(string $xml): array
    {
        return $this->castToArray((array)simplexml_load_string($xml));
    }

    /**
     * Recursively cast to array
     *
     * @param  mixed $input
     *
     * @return array
     */
    private function castToArray($input): array
    {
        $result = array();

        foreach ($input as $key => $value) {
            $result[$key] = $this->castValue($value);
        }

        return $result;
    }

    /**
     * Cast the given $value to an array possible type.
     *
     * @param mixed $value
     *
     * @return mixed
     */
    private function castValue($value)
    {
        if ($value instanceof \SimpleXMLElement) {
            return $this->castToArray((array)$value);
        }

        if (is_array($value)) {
            return $this->castToArray($value);
        }

        if (is_numeric($value)) {
            return $value + 0;
        }

        if (is_string($value) && in_array(strtolower($value), array('true', 'yes', 'on', 'false', 'no', 'off'))) {
            return filter_var($value, FILTER_VALIDATE_BOOLEAN);
        }

        return $value;
    }
}


<?php
/**
 * Spiral Framework.
 *
 * @license   MIT
 * @author    Anton Titov (Wolfy-J)
 */
declare(strict_types=1);

namespace Spiral\Boot;

use Spiral\Boot\Bootloader\CoreBootloader;
use Spiral\Boot\Exception\BootException;
use Spiral\Core\Container;
use Spiral\Core\ContainerScope;

/**
 * Core responsible for application initialization, bootloading of all required services,
 * environment and directory management, exception handling.
 */
abstract class AbstractKernel implements KernelInterface
{
    /**
     * Defines list of bootloaders to be used for core initialisation and all system components.
     */
    protected const SYSTEM = [CoreBootloader::class];

    /**
     * List of bootloaders to be called on application initialization (before `serve` method).
     * This constant must be redefined in child application.
     */
    protected const LOAD = [];

    /** @var Container */
    protected $container;

    /** @var BootloadManager */
    protected $bootloader;

    /** @var DispatcherInterface[] */
    private $dispatchers = [];

    /**
     * @param Container $container
     * @param array     $directories
     */
    public function __construct(Container $container, array $directories)
    {
        $this->container = $container;

        $this->container->bindSingleton(KernelInterface::class, $this);
        $this->container->bindSingleton(self::class, $this);
        $this->container->bindSingleton(static::class, $this);

        $this->container->bindSingleton(
            DirectoriesInterface::class,
            new Directories($this->mapDirectories($directories))
        );

        $this->bootloader = new BootloadManager($this->container);
        $this->bootloader->bootload(static::SYSTEM);
    }

    /**
     * Add new dispatcher. This method must only be called before method `serve`
     * will be invoked.
     *
     * @param DispatcherInterface $dispatcher
     */
    public function addDispatcher(DispatcherInterface $dispatcher)
    {
        $this->dispatchers[] = $dispatcher;
    }

    /**
     * Start application and serve user requests using selected dispatcher or throw
     * an exception.
     *
     * @throws BootException
     * @throws \Throwable
     */
    public function serve()
    {
        foreach ($this->dispatchers as $dispatcher) {
            if ($dispatcher->canServe()) {
                ContainerScope::runScope($this->container, [$dispatcher, 'serve']);

                return;
            }
        }

        throw new BootException("Unable to locate active dispatcher.");
    }

    /**
     * Bootstrap application. Must be executed before start method.
     */
    abstract protected function bootstrap();

    /**
     * Normalizes directory list and adds all required alises.
     *
     * @param array $directories
     * @return array
     */
    abstract protected function mapDirectories(array $directories): array;

    /**
     * Bootload all registered classes using BootloadManager.
     */
    private function bootload()
    {
        $this->bootloader->bootload(static::LOAD);
    }

    /**
     * Initiate application core.
     *
     * @param array                     $directories  Spiral directories should include root,
     *                                                libraries and application directories.
     * @param EnvironmentInterface|null $environment  Application specific environment if any.
     * @param bool                      $handleErrors Enable global error handling.
     * @return self|static
     */
    public static function init(
        array $directories,
        EnvironmentInterface $environment = null,
        bool $handleErrors = true
    ): ?self {
        if ($handleErrors) {
            ExceptionHandler::register();
        }

        $core = new static(new Container(), $directories);
        $core->container->bindSingleton(
            EnvironmentInterface::class,
            $environment ?? new Environment()
        );

        try {
            ContainerScope::runScope($core->container, function () use ($core) {
                $core->bootload();
                $core->bootstrap();
            });
        } catch (\Throwable $e) {
            ExceptionHandler::handleException($e);

            return null;
        }

        return $core;
    }
}