%PDF- %PDF-
Direktori : /www/varak.net/losik.varak.net/vendor/nette/database/src/Database/Table/ |
Current File : //www/varak.net/losik.varak.net/vendor/nette/database/src/Database/Table/Selection.php |
<?php /** * This file is part of the Nette Framework (https://nette.org) * Copyright (c) 2004 David Grudl (https://davidgrudl.com) */ declare(strict_types=1); namespace Nette\Database\Table; use Nette; use Nette\Database\Conventions; use Nette\Database\Explorer; /** * Filtered table representation. * Selection is based on the great library NotORM http://www.notorm.com written by Jakub Vrana. */ class Selection implements \Iterator, IRowContainer, \ArrayAccess, \Countable { use Nette\SmartObject; /** @var Explorer */ protected $explorer; /** @var Explorer back compatibility */ protected $context; /** @var Conventions */ protected $conventions; /** @var Nette\Caching\Cache */ protected $cache; /** @var SqlBuilder */ protected $sqlBuilder; /** @var string table name */ protected $name; /** @var string|string[]|null primary key field name */ protected $primary; /** @var string|bool primary column sequence name, false for autodetection */ protected $primarySequence = false; /** @var ActiveRow[] data read from database in [primary key => ActiveRow] format */ protected $rows; /** @var ActiveRow[] modifiable data in [primary key => ActiveRow] format */ protected $data; /** @var bool */ protected $dataRefreshed = false; /** @var mixed cache array of Selection and GroupedSelection prototypes */ protected $globalRefCache; /** @var mixed */ protected $refCache; /** @var string|null */ protected $generalCacheKey; /** @var string|null */ protected $specificCacheKey; /** @var array of [conditions => [key => ActiveRow]]; used by GroupedSelection */ protected $aggregation = []; /** @var array|false|null of touched columns */ protected $accessedColumns; /** @var array|false|null of earlier touched columns */ protected $previousAccessedColumns; /** @var self|null should instance observe accessed columns caching */ protected $observeCache; /** @var array of primary key values */ protected $keys = []; /** * Creates filtered table representation. */ public function __construct( Explorer $explorer, Conventions $conventions, string $tableName, ?Nette\Caching\IStorage $cacheStorage = null ) { $this->explorer = $this->context = $explorer; $this->conventions = $conventions; $this->name = $tableName; $this->cache = $cacheStorage ? new Nette\Caching\Cache($cacheStorage, 'Nette.Database.' . md5($explorer->getConnection()->getDsn())) : null; $this->primary = $conventions->getPrimary($tableName); $this->sqlBuilder = new SqlBuilder($tableName, $explorer); $this->refCache = &$this->getRefTable($refPath)->globalRefCache[$refPath]; } public function __destruct() { $this->saveCacheState(); } public function __clone() { $this->sqlBuilder = clone $this->sqlBuilder; } public function getName(): string { return $this->name; } /** * @return string|string[]|null */ public function getPrimary(bool $throw = true) { if ($this->primary === null && $throw) { throw new \LogicException("Table '{$this->name}' does not have a primary key."); } return $this->primary; } public function getPrimarySequence(): ?string { if ($this->primarySequence === false) { $this->primarySequence = $this->explorer->getStructure()->getPrimaryKeySequence($this->name); } return $this->primarySequence; } /** * @return static */ public function setPrimarySequence(string $sequence) { $this->primarySequence = $sequence; return $this; } public function getSql(): string { return $this->sqlBuilder->buildSelectQuery($this->getPreviousAccessedColumns()); } /** * Loads cache of previous accessed columns and returns it. * @internal * @return array|bool */ public function getPreviousAccessedColumns() { if ($this->cache && $this->previousAccessedColumns === null) { $this->accessedColumns = $this->previousAccessedColumns = $this->cache->load($this->getGeneralCacheKey()); if ($this->previousAccessedColumns === null) { $this->previousAccessedColumns = []; } } return array_keys(array_filter((array) $this->previousAccessedColumns)); } /** * @internal */ public function getSqlBuilder(): SqlBuilder { return $this->sqlBuilder; } /********************* quick access ****************d*g**/ /** * Returns row specified by primary key. * @param mixed $key primary key */ public function get($key): ?ActiveRow { $clone = clone $this; return $clone->wherePrimary($key)->fetch(); } /** * Fetches single row object. */ public function fetch(): ?ActiveRow { $this->execute(); $return = current($this->data); next($this->data); return $return === false ? null : $return; } /** * Fetches single field. * @return mixed * @deprecated */ public function fetchField(?string $column = null) { if ($column) { $this->select($column); } $row = $this->fetch(); if ($row) { return $column ? $row[$column] : array_values($row->toArray())[0]; } return null; } /** * Fetches all rows as associative array. * @param string|int $key column name used for an array key or null for numeric index * @param string|int $value column name used for an array value or null for the whole row */ public function fetchPairs($key = null, $value = null): array { return Nette\Database\Helpers::toPairs($this->fetchAll(), $key, $value); } /** * Fetches all rows. * @return ActiveRow[] */ public function fetchAll(): array { return iterator_to_array($this); } /** * Fetches all rows and returns associative tree. * @param string $path associative descriptor */ public function fetchAssoc(string $path): array { $rows = array_map('iterator_to_array', $this->fetchAll()); return Nette\Utils\Arrays::associate($rows, $path); } /********************* sql selectors ****************d*g**/ /** * Adds select clause, more calls appends to the end. * @param string|string[] $columns for example "column, MD5(column) AS column_md5" * @return static */ public function select($columns, ...$params) { $this->emptyResultSet(); $this->sqlBuilder->addSelect($columns, ...$params); return $this; } /** * Adds condition for primary key. * @param mixed $key * @return static */ public function wherePrimary($key) { if (is_array($this->primary) && Nette\Utils\Arrays::isList($key)) { if (isset($key[0]) && is_array($key[0])) { $this->where($this->primary, $key); } else { foreach ($this->primary as $i => $primary) { $this->where($this->name . '.' . $primary, $key[$i]); } } } elseif (is_array($key) && !Nette\Utils\Arrays::isList($key)) { // key contains column names $this->where($key); } else { $this->where($this->name . '.' . $this->getPrimary(), $key); } return $this; } /** * Adds where condition, more calls appends with AND. * @param string|array $condition possibly containing ? * @return static */ public function where($condition, ...$params) { $this->condition($condition, $params); return $this; } /** * Adds ON condition when joining specified table, more calls appends with AND. * @param string $tableChain table chain or table alias for which you need additional left join condition * @param string $condition possibly containing ? * @return static */ public function joinWhere(string $tableChain, string $condition, ...$params) { $this->condition($condition, $params, $tableChain); return $this; } /** * Adds condition, more calls appends with AND. * @param string|string[] $condition possibly containing ? */ protected function condition($condition, array $params, $tableChain = null): void { $this->emptyResultSet(); if (is_array($condition) && $params === []) { // where(['column1' => 1, 'column2 > ?' => 2]) foreach ($condition as $key => $val) { if (is_int($key)) { $this->condition($val, [], $tableChain); // where('full condition') } else { $this->condition($key, [$val], $tableChain); // where('column', 1) } } } elseif ($tableChain) { $this->sqlBuilder->addJoinCondition($tableChain, $condition, ...$params); } else { $this->sqlBuilder->addWhere($condition, ...$params); } } /** * Adds where condition using the OR operator between parameters. * More calls appends with AND. * @param array $parameters ['column1' => 1, 'column2 > ?' => 2, 'full condition'] * @return static * @throws Nette\InvalidArgumentException */ public function whereOr(array $parameters) { if (count($parameters) < 2) { return $this->where($parameters); } $columns = []; $values = []; foreach ($parameters as $key => $val) { if (is_int($key)) { // whereOr(['full condition']) $columns[] = $val; } elseif (strpos($key, '?') === false) { // whereOr(['column1' => 1]) $columns[] = $key . ' ?'; $values[] = $val; } else { // whereOr(['column1 > ?' => 1]) $qNumber = substr_count($key, '?'); if ($qNumber > 1 && (!is_array($val) || $qNumber !== count($val))) { throw new Nette\InvalidArgumentException('Argument count does not match placeholder count.'); } $columns[] = $key; $values = array_merge($values, $qNumber > 1 ? $val : [$val]); } } $columnsString = '(' . implode(') OR (', $columns) . ')'; return $this->where($columnsString, $values); } /** * Adds order clause, more calls appends to the end. * @param string $columns for example 'column1, column2 DESC' * @return static */ public function order(string $columns, ...$params) { $this->emptyResultSet(); $this->sqlBuilder->addOrder($columns, ...$params); return $this; } /** * Sets limit clause, more calls rewrite old values. * @return static */ public function limit(?int $limit, ?int $offset = null) { $this->emptyResultSet(); $this->sqlBuilder->setLimit($limit, $offset); return $this; } /** * Sets offset using page number, more calls rewrite old values. * @return static */ public function page(int $page, int $itemsPerPage, &$numOfPages = null) { if (func_num_args() > 2) { $numOfPages = (int) ceil($this->count('*') / $itemsPerPage); } if ($page < 1) { $itemsPerPage = 0; } return $this->limit($itemsPerPage, ($page - 1) * $itemsPerPage); } /** * Sets group clause, more calls rewrite old value. * @return static */ public function group(string $columns, ...$params) { $this->emptyResultSet(); $this->sqlBuilder->setGroup($columns, ...$params); return $this; } /** * Sets having clause, more calls rewrite old value. * @return static */ public function having(string $having, ...$params) { $this->emptyResultSet(); $this->sqlBuilder->setHaving($having, ...$params); return $this; } /** * Aliases table. Example ':book:book_tag.tag', 'tg' * @return static */ public function alias(string $tableChain, string $alias) { $this->sqlBuilder->addAlias($tableChain, $alias); return $this; } /********************* aggregations ****************d*g**/ /** * Executes aggregation function. * @param string $function select call in "FUNCTION(column)" format * @return mixed */ public function aggregation(string $function, ?string $groupFunction = null) { $selection = $this->createSelectionInstance(); $selection->getSqlBuilder()->importConditions($this->getSqlBuilder()); if ($groupFunction && $selection->getSqlBuilder()->importGroupConditions($this->getSqlBuilder())) { $selection->select("$function AS aggregate"); $query = "SELECT $groupFunction(aggregate) AS groupaggregate FROM (" . $selection->getSql() . ') AS aggregates'; return $this->context->query($query, ...$selection->getSqlBuilder()->getParameters())->fetch()->groupaggregate; } else { $selection->select($function); foreach ($selection->fetch() as $val) { return $val; } } } /** * Counts number of rows. * @param string $column if it is not provided returns count of result rows, otherwise runs new sql counting query */ public function count(?string $column = null): int { if (!$column) { $this->execute(); return count($this->data); } return (int) $this->aggregation("COUNT($column)", 'SUM'); } /** * Returns minimum value from a column. * @return mixed */ public function min(string $column) { return $this->aggregation("MIN($column)", 'MIN'); } /** * Returns maximum value from a column. * @return mixed */ public function max(string $column) { return $this->aggregation("MAX($column)", 'MAX'); } /** * Returns sum of values in a column. * @return mixed */ public function sum(string $column) { return $this->aggregation("SUM($column)", 'SUM'); } /********************* internal ****************d*g**/ protected function execute(): void { if ($this->rows !== null) { return; } $this->observeCache = $this; if ($this->primary === null && $this->sqlBuilder->getSelect() === null) { throw new Nette\InvalidStateException('Table with no primary key requires an explicit select clause.'); } try { $result = $this->query($this->getSql()); } catch (Nette\Database\DriverException $exception) { if (!$this->sqlBuilder->getSelect() && $this->previousAccessedColumns) { $this->previousAccessedColumns = false; $this->accessedColumns = []; $result = $this->query($this->getSql()); } else { throw $exception; } } $this->rows = []; $usedPrimary = true; foreach ($result->getPdoStatement() as $key => $row) { $row = $this->createRow($result->normalizeRow($row)); $primary = $row->getSignature(false); $usedPrimary = $usedPrimary && $primary !== ''; $this->rows[$usedPrimary ? $primary : $key] = $row; } $this->data = $this->rows; if ($usedPrimary && $this->accessedColumns !== false) { foreach ((array) $this->primary as $primary) { $this->accessedColumns[$primary] = true; } } } protected function createRow(array $row): ActiveRow { return new ActiveRow($row, $this); } public function createSelectionInstance(?string $table = null): self { return new self($this->explorer, $this->conventions, $table ?: $this->name, $this->cache ? $this->cache->getStorage() : null); } protected function createGroupedSelectionInstance(string $table, string $column): GroupedSelection { return new GroupedSelection($this->explorer, $this->conventions, $table, $column, $this, $this->cache ? $this->cache->getStorage() : null); } protected function query(string $query): Nette\Database\ResultSet { return $this->explorer->query($query, ...$this->sqlBuilder->getParameters()); } protected function emptyResultSet(bool $clearCache = true, bool $deleteRererencedCache = true): void { if ($this->rows !== null && $clearCache) { $this->saveCacheState(); } if ($clearCache) { // NOT NULL in case of missing some column $this->previousAccessedColumns = null; $this->generalCacheKey = null; } $null = null; $this->rows = &$null; $this->specificCacheKey = null; $this->refCache['referencingPrototype'] = []; if ($deleteRererencedCache) { $this->refCache['referenced'] = []; } } protected function saveCacheState(): void { if ( $this->observeCache === $this && $this->cache && !$this->sqlBuilder->getSelect() && $this->accessedColumns !== $this->previousAccessedColumns ) { $previousAccessed = $this->cache->load($this->getGeneralCacheKey()); $accessed = $this->accessedColumns; $needSave = is_array($accessed) && is_array($previousAccessed) ? array_intersect_key($accessed, $previousAccessed) !== $accessed : $accessed !== $previousAccessed; if ($needSave) { $save = is_array($accessed) && is_array($previousAccessed) ? $previousAccessed + $accessed : $accessed; $this->cache->save($this->getGeneralCacheKey(), $save); $this->previousAccessedColumns = null; } } } /** * Returns Selection parent for caching. * @return static */ protected function getRefTable(&$refPath) { return $this; } /** * Loads refCache references */ protected function loadRefCache(): void { } /** * Returns general cache key independent on query parameters or sql limit * Used e.g. for previously accessed columns caching */ protected function getGeneralCacheKey(): string { if ($this->generalCacheKey) { return $this->generalCacheKey; } $key = [self::class, $this->name, $this->sqlBuilder->getConditions()]; $trace = []; foreach (debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS) as $item) { $trace[] = isset($item['file'], $item['line']) ? $item['file'] . $item['line'] : null; } $key[] = $trace; return $this->generalCacheKey = md5(serialize($key)); } /** * Returns object specific cache key dependent on query parameters * Used e.g. for reference memory caching */ protected function getSpecificCacheKey(): string { if ($this->specificCacheKey) { return $this->specificCacheKey; } return $this->specificCacheKey = $this->sqlBuilder->getSelectQueryHash($this->getPreviousAccessedColumns()); } /** * @internal * @param string|null column name or null to reload all columns * @return bool if selection requeried for more columns. */ public function accessColumn(?string $key, bool $selectColumn = true): bool { if (!$this->cache) { return false; } if ($key === null) { $this->accessedColumns = false; $currentKey = key((array) $this->data); } elseif ($this->accessedColumns !== false) { $this->accessedColumns[$key] = $selectColumn; } if ( $selectColumn && $this->previousAccessedColumns && ( $key === null || !isset($this->previousAccessedColumns[$key]) ) && !$this->sqlBuilder->getSelect() ) { if ($this->sqlBuilder->getLimit()) { $generalCacheKey = $this->generalCacheKey; $sqlBuilder = $this->sqlBuilder; $primaryValues = []; foreach ((array) $this->rows as $row) { $primary = $row->getPrimary(); $primaryValues[] = is_array($primary) ? array_values($primary) : $primary; } $this->emptyResultSet(false); $this->sqlBuilder = clone $this->sqlBuilder; $this->sqlBuilder->setLimit(null, null); $this->wherePrimary($primaryValues); $this->generalCacheKey = $generalCacheKey; $this->previousAccessedColumns = []; $this->execute(); $this->sqlBuilder = $sqlBuilder; } else { $this->emptyResultSet(false); $this->previousAccessedColumns = []; $this->execute(); } $this->dataRefreshed = true; // move iterator to specific key if (isset($currentKey)) { while (key($this->data) !== null && key($this->data) !== $currentKey) { next($this->data); } } } return $this->dataRefreshed; } /** * @internal */ public function removeAccessColumn(string $key): void { if ($this->cache && is_array($this->accessedColumns)) { $this->accessedColumns[$key] = false; } } /** * Returns if selection requeried for more columns. */ public function getDataRefreshed(): bool { return $this->dataRefreshed; } /********************* manipulation ****************d*g**/ /** * Inserts row in a table. * @param array|\Traversable|Selection $data [$column => $value]|\Traversable|Selection for INSERT ... SELECT * @return ActiveRow|int|bool Returns ActiveRow or number of affected rows for Selection or table without primary key */ public function insert(iterable $data) { //should be called before query for not to spoil PDO::lastInsertId $primarySequenceName = $this->getPrimarySequence(); $primaryAutoincrementKey = $this->explorer->getStructure()->getPrimaryAutoincrementKey($this->name); if ($data instanceof self) { $return = $this->explorer->query($this->sqlBuilder->buildInsertQuery() . ' ' . $data->getSql(), ...$data->getSqlBuilder()->getParameters()); } else { if ($data instanceof \Traversable) { $data = iterator_to_array($data); } $return = $this->explorer->query($this->sqlBuilder->buildInsertQuery() . ' ?values', $data); } $this->loadRefCache(); if ($data instanceof self || $this->primary === null) { unset($this->refCache['referencing'][$this->getGeneralCacheKey()][$this->getSpecificCacheKey()]); return $return->getRowCount(); } $primaryKey = []; foreach ((array) $this->primary as $key) { if (isset($data[$key])) { $primaryKey[$key] = $data[$key]; } } // First check sequence if (!empty($primarySequenceName) && $primaryAutoincrementKey) { $primaryKey[$primaryAutoincrementKey] = $this->explorer->getInsertId($this->explorer->getConnection()->getDriver()->delimite($primarySequenceName)); // Autoincrement primary without sequence } elseif ($primaryAutoincrementKey) { $primaryKey[$primaryAutoincrementKey] = $this->explorer->getInsertId($primarySequenceName); // Multi column primary without autoincrement } elseif (is_array($this->primary)) { foreach ($this->primary as $key) { if (!isset($data[$key])) { return $data; } } // Primary without autoincrement, try get primary from inserting data } elseif ($this->primary && isset($data[$this->primary])) { $primaryKey = $data[$this->primary]; // If primaryKey cannot be prepared, return inserted rows count } else { unset($this->refCache['referencing'][$this->getGeneralCacheKey()][$this->getSpecificCacheKey()]); return $return->getRowCount(); } $row = $this->createSelectionInstance() ->select('*') ->wherePrimary($primaryKey) ->fetch(); if ($this->rows !== null) { if ($signature = $row->getSignature(false)) { $this->rows[$signature] = $row; $this->data[$signature] = $row; } else { $this->rows[] = $row; $this->data[] = $row; } } return $row; } /** * Updates all rows in result set. * Joins in UPDATE are supported only in MySQL * @return int number of affected rows */ public function update(iterable $data): int { if ($data instanceof \Traversable) { $data = iterator_to_array($data); } elseif (!is_array($data)) { throw new Nette\InvalidArgumentException; } if (!$data) { return 0; } return $this->explorer->query( $this->sqlBuilder->buildUpdateQuery(), ...array_merge([$data], $this->sqlBuilder->getParameters()) )->getRowCount(); } /** * Deletes all rows in result set. * @return int number of affected rows */ public function delete(): int { return $this->query($this->sqlBuilder->buildDeleteQuery())->getRowCount(); } /********************* references ****************d*g**/ /** * Returns referenced row. * @return ActiveRow|false|null null if the row does not exist, false if the relationship does not exist */ public function getReferencedTable(ActiveRow $row, ?string $table, ?string $column = null) { if (!$column) { $belongsTo = $this->conventions->getBelongsToReference($this->name, $table); if (!$belongsTo) { return false; } [$table, $column] = $belongsTo; } if (!$row->accessColumn($column)) { return false; } $checkPrimaryKey = $row[$column]; $referenced = &$this->refCache['referenced'][$this->getSpecificCacheKey()]["$table.$column"]; $selection = &$referenced['selection']; $cacheKeys = &$referenced['cacheKeys']; if ($selection === null || ($checkPrimaryKey !== null && !isset($cacheKeys[$checkPrimaryKey]))) { $this->execute(); $cacheKeys = []; foreach ($this->rows as $row) { if ($row[$column] === null) { continue; } $key = $row[$column]; $cacheKeys[$key] = true; } if ($cacheKeys) { $selection = $this->createSelectionInstance($table); $selection->where($selection->getPrimary(), array_keys($cacheKeys)); } else { $selection = []; } } return $selection[$checkPrimaryKey] ?? null; } /** * Returns referencing rows. * @param int|string $active primary key */ public function getReferencingTable(string $table, ?string $column = null, $active = null): ?GroupedSelection { if (strpos($table, '.') !== false) { [$table, $column] = explode('.', $table); } elseif (!$column) { $hasMany = $this->conventions->getHasManyReference($this->name, $table); if (!$hasMany) { return null; } [$table, $column] = $hasMany; } $prototype = &$this->refCache['referencingPrototype'][$this->getSpecificCacheKey()]["$table.$column"]; if (!$prototype) { $prototype = $this->createGroupedSelectionInstance($table, $column); $prototype->where("$table.$column", array_keys((array) $this->rows)); } $clone = clone $prototype; $clone->setActive($active); return $clone; } /********************* interface Iterator ****************d*g**/ public function rewind(): void { $this->execute(); $this->keys = array_keys($this->data); reset($this->keys); } /** @return ActiveRow|false */ #[\ReturnTypeWillChange] public function current() { return ($key = current($this->keys)) !== false ? $this->data[$key] : false; } /** * @return string|int row ID */ #[\ReturnTypeWillChange] public function key() { return current($this->keys); } public function next(): void { do { next($this->keys); } while (($key = current($this->keys)) !== false && !isset($this->data[$key])); } public function valid(): bool { return current($this->keys) !== false; } /********************* interface ArrayAccess ****************d*g**/ /** * Mimic row. * @param string $key * @param ActiveRow $value */ public function offsetSet($key, $value): void { $this->execute(); $this->rows[$key] = $value; } /** * Returns specified row. * @param string $key */ public function offsetGet($key): ?ActiveRow { $this->execute(); return $this->rows[$key]; } /** * Tests if row exists. * @param string $key */ public function offsetExists($key): bool { $this->execute(); return isset($this->rows[$key]); } /** * Removes row from result set. * @param string $key */ public function offsetUnset($key): void { $this->execute(); unset($this->rows[$key], $this->data[$key]); } }