evaluateClosures($selects));
}
public function from($from, $alias = null)
{
$from = $this->evaluateClosures([$from])[0];
return call_user_func_array([parent::class, __FUNCTION__], [$from, $alias]);
}
public function addSelect($selects = null)
{
$selects = func_get_args();
return call_user_func_array([parent::class, __FUNCTION__], $this->evaluateClosures($selects));
}
public function set($key, $value)
{
[$key, $value] = $this->evaluateClosures([$key, $value]);
return call_user_func_array([parent::class, __FUNCTION__], [$key, $value]);
}
/**
* @return $this
*/
public function where($predicates)
{
$predicates = func_get_args();
if (isDevelopment()) {
if ($this->getQueryPart('where') !== null) {
/* @noinspection PhpUnhandledExceptionInspection */
throw new \Exception('This call of \'where\' method will reset where query part! Use \'andWhere\' instead.');
}
}
return call_user_func_array([parent::class, __FUNCTION__], $this->evaluateClosures($predicates));
}
/**
* @return $this
*/
public function andWhere($where)
{
$where = func_get_args();
$where = $this->evaluateClosures($where);
// Remove null wheres - caused by specs
$where = array_filter($where, function ($x) {
return $x !== null;
});
if (!count($where)) {
return $this;
}
return call_user_func_array([parent::class, __FUNCTION__], $where);
}
/**
* @return $this
*/
public function orWhere($where)
{
$where = func_get_args();
return call_user_func_array([parent::class, __FUNCTION__], $this->evaluateClosures($where));
}
/**
* @param string $productsAlias
*
* @return \Query\QueryBuilder
*/
public function fromProducts($productsAlias = 'p')
{
return $this->from('products', $productsAlias);
}
/**
* @return array
*
* @internal
*/
public function evaluateClosures(array $predicates)
{
foreach ($predicates as &$predicate) {
while ($predicate instanceof \Closure) {
$predicate = $predicate($this);
}
}
return $predicates;
}
public function add($sqlPartName, $sqlPart, $append = false)
{
if ($sqlPartName === 'join') {
return $this->addJoin(current($sqlPart)['joinAlias'], $sqlPart, $append);
}
if ($sqlPartName === 'select' && $append) {
$selects = $this->getQueryPart('select');
foreach ($sqlPart as $part) {
if (in_array($part, $selects)) {
return $this;
}
}
}
return parent::add($sqlPartName, $sqlPart, $append);
}
public function addParameters(array $parameters, array $types = [])
{
foreach ($parameters as $name => $value) {
$this->setParameter($name, $value, $types[$name] ?? null);
}
return $this;
}
public function orderBySql($orderBySql)
{
return $this->add('orderBy', $orderBySql);
}
public function addSubselect(QueryBuilder $qb, $alias, $template = '{}')
{
$fields = str_replace('{}', "({$qb->getSQL()})", $template);
return $this->addSelect("{$fields} AS {$alias}")
->addParameters($qb->getParameters(), $qb->getParameterTypes());
}
public function joinSubQuery($fromAlias, QueryBuilder $qb, $alias, $condition)
{
return $this->join($fromAlias, '('.$qb->getSQL().')', $alias, $condition)
->addParameters($qb->getParameters(), $qb->getParameterTypes());
}
public function leftJoinSubQuery($fromAlias, QueryBuilder $qb, $alias, $condition)
{
return $this->leftJoin($fromAlias, '('.$qb->getSQL().')', $alias, $condition)
->addParameters($qb->getParameters(), $qb->getParameterTypes());
}
public function crossJoin(string $fromAlias, string $table, string $alias): self
{
return $this->add('join', [
$fromAlias => [
'joinType' => 'cross',
'joinTable' => $table,
'joinAlias' => $alias,
'joinCondition' => null,
],
], true);
}
private function addJoin($joinAlias, $joinDeclaration, $append)
{
$normalizedJoin = mb_strtolower(preg_replace('/\s/', '', implode(current($joinDeclaration))));
if ($this->isJoinPresent($joinAlias, $normalizedJoin)) {
return $this;
}
$this->joins[$joinAlias] = $normalizedJoin;
return parent::add('join', $joinDeclaration, $append);
}
private function isJoinPresent($joinAlias, $normalizedJoin)
{
if (isset($this->joins[$joinAlias])) {
if ($this->joins[$joinAlias] !== $normalizedJoin) {
$existingNormalizedJoin = $this->joins[$joinAlias];
throw new \LogicException("Alias '{$joinAlias}' has already been set with the declaration '{$existingNormalizedJoin}'. Now trying: '{$normalizedJoin}'");
}
return true;
}
return false;
}
public function isAliasPresent($alias)
{
$fromAlias = $this->getQueryPart('from')[0]['alias'] ?? $this->getQueryPart('from')['alias'] ?? '';
return ($fromAlias == $alias) || isset($this->joins[$alias]);
}
public function execute()
{
wpj_debug([$this->getSQL(), $this->getParameters()]);
return parent::execute();
}
public function setUnbufferedMode(): QueryBuilder
{
$this->getConnection()->getWrappedConnection()->setAttribute(\PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false);
return $this;
}
public function joinVatsOnProducts()
{
$vatContext = Contexts::get(VatContext::class);
if ($vatContext->isCountryOssActive()) {
$countryContext = Contexts::get(CountryContext::class);
$defaultCn = \Settings::getDefault()['oss_vats']['default'] ?? null;
$this->leftJoin('p', 'vats_cns', 'vatcns', 'COALESCE(p.id_cn, :defaultCn) = vatcns.id_cn')
->leftJoin('vatcns', 'vats', 'v', 'vatcns.id_vat = v.id')
->andWhere(
Operator::orX(
Operator::equals(['v.id_country ' => $countryContext->getActiveId()]),
'vatcns.id_cn IS NULL'
)
)
->setParameter('defaultCn', $defaultCn);
} else {
$this->leftJoin('p', 'vats', 'v', 'p.vat = v.id');
}
return $this;
}
public function joinVariationsOnProducts()
{
$this->leftJoin('p', 'products_variations', 'pv', 'p.id = pv.id_product');
return $this;
}
public function joinSectionsOnProducts()
{
$this->join('p', 'products_in_sections', 'ps', 'p.id = ps.id_product');
return $this;
}
public function joinProducersOnProducts()
{
$this->leftJoin('p', 'producers', 'pr', 'p.producer = pr.id');
return $this;
}
public function joinTemplatesOnProducts()
{
$this->leftJoin('p', 'templates_products', 'tp', 'p.id = tp.id_product');
$this->join('tp', 'templates', 't', 't.id = tp.id_template');
return $this;
}
public function joinVariationsLabelsAndValues()
{
$this->join('pv', 'products_variations_combination', 'pvc', 'pv.id = pvc.id_variation')
->join('pvc', 'products_variations_choices_labels', 'pvcl', 'pvc.id_label=pvcl.id')
->join('pvc', 'products_variations_choices_values', 'pvcv', 'pvc.id_value=pvcv.id')
->join('pvc', 'products_variations_choices_categorization', 'pvcc', 'pvc.id_label=pvcc.id_label and pv.id_product=pvcc.id_product')
->orderBy('pvcc.list_order')
->addOrderBy('pvcv.sort');
return $this;
}
public function joinParametersOnProducts($parametersAlias = 'pap')
{
$this->join('p', 'parameters_products', $parametersAlias, "p.id = {$parametersAlias}.id_product");
return $this;
}
public function joinProductsOfSuppliers(string $productAlias = 'p')
{
// if the spaceship operator causes problems, return it to ((pv.id=pos.id_variation) OR (pv.id IS NULL and pos.id_variation IS NULL))
$this->leftJoin($productAlias, 'products_of_suppliers', 'pos', "{$productAlias}.id = pos.id_product AND pv.id <=> pos.id_variation");
return $this;
}
public function joinPriceListsProducts(?int $priceListId = null, string $productAlias = 'p', string $variationAlias = 'pv'): self
{
$priceListId ??= Contexts::get(PricelistContext::class)->getActiveId();
if ($priceListId) {
$priceListIds = \KupShop\PricelistBundle\Query\Product::getActivePriceLists((int) $priceListId);
$this->setParameter('id_pricelist', $priceListIds, Connection::PARAM_INT_ARRAY)
->leftJoin('p', 'pricelists_products', 'prlp', "prlp.id_product = {$productAlias}.id AND prlp.id_variation IS null AND prlp.id_pricelist IN (:id_pricelist)")
->leftJoin('p', 'pricelists_products', 'prlv', "prlv.id_product = {$productAlias}.id AND prlv.id_variation = {$variationAlias}.id AND prlv.id_pricelist IN (:id_pricelist)")
->leftJoin('prlv', 'pricelists', 'prl', 'prlp.id_pricelist = prl.id')
->leftJoin('prlv', 'pricelists', 'prl_v', 'prlv.id_pricelist = prl_v.id');
if (count($priceListIds) <= 1) {
$this->setForceIndexForJoin('prlp', 'pricelist_product')
->setForceIndexForJoin('prlv', 'pricelist_product');
}
}
return $this;
}
/**
* @return QueryBuilder
* Specifies values for an multi insert query indexed by column names.
* Replaces any previous values, if any.
*
*
* $array =[
* ['name' => '?', 'password' => '?'],
* ['name' => '?', 'password' => '?']
* ];
*
*/
public function multiValues(array $values): QueryBuilder
{
if ($this->getType() !== self::INSERT) {
throw new \InvalidArgumentException('multiValues without INSERT');
}
$this->setMultiInsert(true);
return $this->values($values);
}
/**
* @return QueryBuilder
* Specifies values for a multi insert query indexed by column names.
* One row insert per call
*
* $values =['name' => '?', 'password' => '?']
*
*/
public function multiDirectValues(array $values, array $types = []): QueryBuilder
{
$sqlParts = $this->getQueryParts();
if ($this->getType() !== self::INSERT) {
throw new \InvalidArgumentException('multiValues without INSERT');
}
if (!$this->getMultiInsert()) {
$this->multiValues([]); // vyčistíme values
}
$pf = 'multi'.count($sqlParts['values']).'_';
$array = [];
foreach ($values as $param => $value) {
$key = ":{$pf}{$param}";
$this->setParameter($key, $value, isset($types[$param]) ? $types[$param] : null);
$array['`'.$param.'`'] = $key;
}
$sqlParts['values'][] = $array;
return $this->multiValues($sqlParts['values']);
}
protected function setMultiInsert(bool $multiInsert)
{
$this->multiInsert = $multiInsert;
}
protected function getMultiInsert(): bool
{
return $this->multiInsert;
}
/**
* Specifies values for an insert query indexed by column names
* without need to use parameters()
* Replaces any previous parameters with same name.
*
*
* $qb = $conn->createQueryBuilder()
* ->insert('users')
* ->directValues(
* array(
* 'name' => 'test',
* 'password' => 10
* )
* );
*
*
* @param array $values the values to specify for the insert query indexed by column names
*
* @return $this
*/
public function directValues(array $values, array $types = [])
{
if ($this->getMultiInsert()) {
throw new \InvalidArgumentException('directValues mixed with multiInsertValues');
}
foreach ($values as $param => $value) {
$key = ":{$param}";
if ($this->getType() == self::INSERT) {
$this->setValue('`'.$param.'`', $key);
} elseif ($this->getType() == self::UPDATE) {
$this->set('`'.$param.'`', $key);
} elseif ($this->getType() == self::DELETE) {
throw new \InvalidArgumentException('directValues with DELETE not implemented');
}
$this->setParameter($param, $value, isset($types[$param]) ? $types[$param] : null);
}
return $this;
}
public function getSQL()
{
$hasLimit = $this->getMaxResults() !== null && $this->getMaxResults() > 0;
// custom support for JOINs in UPDATE statements
if ($this->getType() == self::UPDATE && (!empty($this->getQueryPart('join')) || $hasLimit)) {
return $this->getSQLForUpdate();
}
if ($this->getType() === self::INSERT) {
return $this->getSQLForInsert();
}
// custom support for JOINs in DELETE statements
$tableAlias = $this->getQueryPart('from')[0]['alias'] ?? $this->getQueryPart('from')['alias'] ?? '';
// we should use custom supports in DELETE statements even if there is table alias without JOINs
if ($this->getType() == self::DELETE && (!empty($this->getQueryPart('join')) || !empty($tableAlias) || $hasLimit)) {
return $this->getSQLForDelete();
}
if ($this->getType() == self::SELECT && $this->calcRows) {
$selectPart = $this->getQueryPart('select');
if (!empty($selectPart) && strpos($selectPart[0], 'SQL_CALC_FOUND_ROWS') === false) {
$selectPart[0] = 'SQL_CALC_FOUND_ROWS '.$selectPart[0];
$this->select($selectPart);
}
}
$sql = parent::getSQL();
if ($this->forceIndexes) {
$sql = $this->modifySqlAddForceIndexes($sql);
}
if ($this->withExpressions) {
$sql = $this->modifySqlAddWithExpressions($sql);
}
return $sql.($this->forUpdate ? ' FOR UPDATE' : '').($this->sendToMaster ? ' -- maxscale route to master' : '');
}
private function getSQLForUpdate()
{
$sqlParts = $this->getQueryParts();
// Hack: Njncsdd, je to vsechno private a radsi reflection nez kopirovat pulku QB
$reflector = new \ReflectionClass(\Doctrine\DBAL\Query\QueryBuilder::class);
$method = $reflector->getMethod('getFromClauses');
$method->setAccessible(true);
// UPDATE has different "from" structure than SELECT
$from = $this->getQueryPart('from');
if (!isset($from[0])) {
$this->resetQueryPart('from')
->add('from', $from, true);
}
$table = implode(', ', $method->invoke($this));
$query = 'UPDATE '.$table
.' SET '.implode(', ', $sqlParts['set'])
.($sqlParts['where'] !== null ? ' WHERE '.((string) $sqlParts['where']) : '');
if ($orderBy = $sqlParts['orderBy'] ?? []) {
$query .= ' ORDER BY '.implode(', ', $orderBy);
}
if (null !== ($limit = $this->getMaxResults()) && $limit > 0) {
$query .= ' LIMIT '.$limit;
}
return $query;
}
private function getSQLForDelete()
{
$sqlParts = $this->getQueryParts();
// Hack: Njncsdd, je to vsechno private a radsi reflection nez kopirovat pulku QB
$reflector = new \ReflectionClass(\Doctrine\DBAL\Query\QueryBuilder::class);
$method = $reflector->getMethod('getFromClauses');
$method->setAccessible(true);
$tableAlias = $this->getQueryPart('from')[0]['alias'] ?? '';
$isLimitQuery = $this->getMaxResults() !== null && $this->getMaxResults() > 0;
// https://mariadb.com/kb/en/delete/#description
if (($isLimitQuery || !empty($sqlParts['orderBy'])) && (
!empty($sqlParts['join'])
|| $tableAlias
|| (isset($from['table'], $from[0]) && $from['table'] !== $from[0]['table'])
)) {
throw new \Doctrine\DBAL\Exception('Cannot use multi-table syntax with LIMIT or ORDER BY. Remove any joins, multi-table expressions or table aliases in DELETE FROM.');
}
// UPDATE has different "from" structure than SELECT
$from = $this->getQueryPart('from');
if (!isset($from[0])) {
$this->resetQueryPart('from')
->add('from', $from, true);
}
$fromTable = implode(', ', $method->invoke($this));
$query = 'DELETE'.($tableAlias ? " {$tableAlias}" : '').' FROM '.$fromTable.($sqlParts['where'] !== null ? ' WHERE '.((string) $sqlParts['where']) : '');
if ($orderBy = $sqlParts['orderBy'] ?? []) {
$query .= ' ORDER BY '.implode(', ', $orderBy);
}
if ($isLimitQuery) {
$query .= ' LIMIT '.$this->getMaxResults();
}
return $query;
}
private function getSQLForInsert()
{
$sqlParts = $this->getQueryParts();
return 'INSERT INTO '.$sqlParts['from']['table'].$this->getSQLForValues().$this->getSQLForOnDuplicateKeyUpdate();
}
/**
* Implements support for multi-insert.
*
* @return string
*/
private function getSQLForValues()
{
$sqlParts = $this->getQueryParts();
$values = [];
$columnsToUpdate = [];
if ($this->getMultiInsert()) {
$first = reset($sqlParts['values']);
$columnsToUpdate = array_keys($first);
foreach ($sqlParts['values'] as $vls) {
$values[] = '('.implode(', ', $vls).')';
}
} else {
$columnsToUpdate = array_keys($sqlParts['values']);
$values[] = '('.implode(', ', $sqlParts['values']).')';
}
return ' ('.implode(',', $columnsToUpdate).') VALUES '.implode(',', $values).' ';
}
protected function modifySqlAddForceIndexes($sql)
{
$sqlParts = $this->getQueryParts()['join'];
$joinsToModify = [];
foreach ($sqlParts as $tableJoins) {
foreach ($tableJoins as $join) {
if (array_key_exists($join['joinAlias'], $this->forceIndexes)) {
$joinsToModify[] = $join;
}
}
}
$getJoin = function ($join, $index = '') {
$sql = 'JOIN '.$join['joinTable'].' '.$join['joinAlias'];
if ($index) {
$sql .= " FORCE INDEX ({$index})";
}
return $sql.' ON';
};
foreach ($joinsToModify as $join) {
$replacedCount = 0;
$sql = str_replace($getJoin($join), $getJoin($join, $this->forceIndexes[$join['joinAlias']]), $sql, $replacedCount);
if ($replacedCount > 1) {
throw new \LogicException('QueryBuilder tried to add FORCE UPDATE clause to join, but multiple matching joins were found for the alias '.$join['joinAlias']);
}
}
return $sql;
}
public function setForceIndexForJoin($joinAlias, $indexName)
{
$this->forceIndexes[$joinAlias] = $indexName;
return $this;
}
public function withExpression(string $alias, QueryBuilder $qb, ?int $priority = null)
{
$data = [
'sql' => $qb->getSQL(),
'alias' => $alias,
];
if ($priority) {
$this->withExpressions[$priority] = $data;
} else {
$this->withExpressions[] = $data;
}
$this->addParameters($qb->getParameters(), $qb->getParameterTypes());
return $this;
}
protected function modifySqlAddWithExpressions($sql): string
{
$withSql = 'WITH ';
foreach ($this->withExpressions as $with) {
$withSql .= $with['alias'].' AS ('.$with['sql'].')';
if (next($this->withExpressions)) {
$withSql .= ', ';
}
}
return $withSql.' '.$sql;
}
/**
* Implements support for ON DUPLICATE KEY UPDATE.
*
* @return string
*/
private function getSQLForOnDuplicateKeyUpdate()
{
$onDuplicateKeyUpdate = '';
// build on duplicate key update
if ($this->onDuplicateKeyUpdate) {
$onDuplicateKeyUpdate = ' ON DUPLICATE KEY UPDATE ';
$duplicateKeyValues = [];
foreach ($this->onDuplicateKeyUpdate as $column => $value) {
$duplicateKeyValues[] = is_numeric($column) ? $value.'=VALUES('.$value.')' : $column.'='.$value;
}
$onDuplicateKeyUpdate .= implode(', ', $duplicateKeyValues).' ';
}
return $onDuplicateKeyUpdate;
}
/** Používat jen v krajní nouzi */
public function removeJoin($table, $table2)
{
unset($this->joins[$table2]);
$parts = $this->getQueryPart('join');
$this->resetQueryPart('join');
foreach ($parts as $tmp_table => $tmp_part) {
foreach ($tmp_part as $part) {
if ($part['joinAlias'] != $table2) {
parent::add('join', [$tmp_table => $part], true);
}
}
}
}
public function addQueryBuilderParameters(QueryBuilder $qb): QueryBuilder
{
$this->addParameters($qb->getParameters(), $qb->getParameterTypes());
return $this;
}
public function addCalcRows()
{
$this->calcRows = true;
return $this;
}
public function onDuplicateKeyUpdate(array $values): QueryBuilder
{
if ($this->getType() !== self::INSERT) {
throw new \InvalidArgumentException('Call of "onDuplicateKeyUpdate" is allowed only with insert query');
}
$this->onDuplicateKeyUpdate = $values;
return $this;
}
public function sendToMaster()
{
$this->sendToMaster = true;
return $this;
}
public function forUpdate()
{
$this->forUpdate = true;
return $this;
}
/**
* Using on queries with ? may not work.
*
* @return string SQL with inserted params
*/
public function getRunnableSQL(): string
{
$util = new DoctrineExtension();
$sql = $util->replaceQueryParameters($this->getSQL(), $this->getParameters());
return $util->formatSql($sql, false);
}
}
if (empty($subclass)) {
class QueryBuilder extends QueryBuilderBase
{
}
}