getAll(); } /** * Make an item and populate with given data * * @param array $a Associative array of data to populate * @return Saveable|WireData|Wire * @throws WireException * @since 3.0.146 * */ public function makeItem(array $a = array()) { $item = $this->makeBlankItem(); $this->wire($item); foreach($a as $key => $value) { $item->$key = $value; } $item->resetTrackChanges(true); return $item; } /** * Return the name of the table that this DAO stores item records in * * @return string * */ abstract public function getTable(); /** * Return the default name of the field that load() should sort by (default is none) * * This is overridden by selectors if applied during the load method * * @return string * */ public function getSort() { return ''; } /** * Provides additions to the ___load query for when selectors or selector string are provided * * @param Selectors $selectors * @param DatabaseQuerySelect $query * @throws WireException * @return DatabaseQuerySelect * */ protected function getLoadQuerySelectors($selectors, DatabaseQuerySelect $query) { $database = $this->wire()->database; if($selectors instanceof Selectors) { // iterable selectors } else if($selectors && is_string($selectors)) { // selector string, convert to iterable selectors $selectorString = $selectors; /** @var Selectors $selectors */ $selectors = $this->wire(new Selectors()); $selectors->init($selectorString); } else { // nothing provided, load all assumed return $query; } // Note: ProcessWire core does not appear to ever reach this point as the // core does not use selectors to load any of its WireSaveableItems $functionFields = array( 'sort' => '', 'limit' => '', 'start' => '', ); $item = $this->makeBlankItem(); $fields = array_keys($item->getTableData()); foreach($selectors as $selector) { if(!$database->isOperator($selector->operator)) { throw new WireException("Operator '$selector->operator' may not be used in {$this->className}::load()"); } if(isset($functionFields[$selector->field])) { $functionFields[$selector->field] = $selector->value; continue; } if(!in_array($selector->field, $fields)) { throw new WireException("Field '$selector->field' is not valid for {$this->className}::load()"); } $selectorField = $database->escapeTableCol($selector->field); $query->where("$selectorField$selector->operator?", $selector->value); // QA } $sort = $functionFields['sort']; if($sort && in_array($sort, $fields)) { $query->orderby($database->escapeCol($sort)); } $limit = (int) $functionFields['limit']; if($limit) { $start = $functionFields['start']; $query->limit(($start ? ((int) $start) . ',' : '') . $limit); } return $query; } /** * Get the DatabaseQuerySelect to perform the load operation of items * * @param Selectors|string|null $selectors Selectors or a selector string to find, or NULL to load all. * @return DatabaseQuerySelect * */ protected function getLoadQuery($selectors = null) { $item = $this->makeBlankItem(); $fields = array_keys($item->getTableData()); $database = $this->wire()->database; $table = $database->escapeTable($this->getTable()); foreach($fields as $k => $v) { $v = $database->escapeCol($v); $fields[$k] = "$table.$v"; } /** @var DatabaseQuerySelect $query */ $query = $this->wire(new DatabaseQuerySelect()); $query->select($fields)->from($table); if($sort = $this->getSort()) $query->orderby($sort); $this->getLoadQuerySelectors($selectors, $query); return $query; } /** * Load items from the database table and return them in the same type class that getAll() returns * A selector string or Selectors may be provided so that this can be used as a find() by descending classes that don't load all items at once. * * @param WireArray $items * @param Selectors|string|null $selectors Selectors or a selector string to find, or NULL to load all. * @return WireArray Returns the same type as specified in the getAll() method. * */ protected function ___load(WireArray $items, $selectors = null) { $useLazy = $this->useLazy(); $database = $this->wire()->database; $sql = $this->getLoadQuery($selectors)->getQuery(); $query = $database->prepare($sql); $query->execute(); $rows = $query->fetchAll(\PDO::FETCH_ASSOC); $n = 0; foreach($rows as $row) { if($useLazy) { $this->lazyItems[$n] = $row; $this->lazyNameIndex[$row['name']] = $n; $this->lazyIdIndex[$row['id']] = $n; $n++; } else { $this->initItem($row, $items); } } $query->closeCursor(); $items->setTrackChanges(true); return $items; } /** * Create a new Saveable item from a raw array ($row) and add it to $items * * @param array $row * @param WireArray|null $items * @return Saveable|WireData|Wire * @since 3.0.194 * */ protected function initItem(array &$row, WireArray $items = null) { if(!empty($row['data'])) { if(is_string($row['data'])) $row['data'] = $this->decodeData($row['data']); } else { unset($row['data']); } if($items === null) $items = $this->getWireArray(); $item = $this->makeItem($row); if($item) { if($this->useLazy() && $item->id) $this->unsetLazy($item); $items->add($item); } return $item; } /** * Should the given item key/field be saved in the database? * * Template method used by ___save() * * @param string $key * @return bool * */ protected function saveItemKey($key) { if($key === 'id') return false; return true; } /** * Save the provided item to database * * @param Saveable $item The item to save * @return bool Returns true on success, false on failure * @throws WireException * */ public function ___save(Saveable $item) { $blank = $this->makeBlankItem(); if(!$item instanceof $blank) { $className = $blank->className(); throw new WireException("WireSaveableItems::save(item) requires item to be of type: $className"); } $database = $this->wire()->database; $table = $database->escapeTable($this->getTable()); $sql = "`$table` SET "; $id = (int) $item->id; $this->saveReady($item); $data = $item->getTableData(); $binds = array(); $namePrevious = false; if($id && $item->isChanged('name')) { $query = $database->prepare("SELECT name FROM `$table` WHERE id=:id"); $query->bindValue(':id', $id, \PDO::PARAM_INT); $query->execute(); $oldName = $query->fetchColumn(); $query->closeCursor(); if($oldName != $item->name) $namePrevious = $oldName; if($namePrevious) $this->renameReady($item, $namePrevious, $item->name); } foreach($data as $key => $value) { if(!$this->saveItemKey($key)) continue; if($key === 'data') $value = is_array($value) ? $this->encodeData($value) : ''; $key = $database->escapeTableCol($key); $bindKey = $database->escapeCol($key); $binds[":$bindKey"] = $value; $sql .= "`$key`=:$bindKey, "; } $sql = rtrim($sql, ", "); if($id) { $query = $database->prepare("UPDATE $sql WHERE id=:id"); foreach($binds as $key => $value) { $query->bindValue($key, $value); } $query->bindValue(":id", $id, \PDO::PARAM_INT); $result = $query->execute(); } else { $query = $database->prepare("INSERT INTO $sql"); foreach($binds as $key => $value) { $query->bindValue($key, $value); } $result = $query->execute(); if($result) { $item->id = (int) $database->lastInsertId(); $this->getWireArray()->add($item); $this->added($item); } } if($result) { if($namePrevious) $this->renamed($item, $namePrevious, $item->name); $this->saved($item); $this->resetTrackChanges(); } else { $this->error("Error saving '$item'"); } return $result; } /** * Delete the provided item from the database * * @param Saveable $item Item to save * @return bool Returns true on success, false on failure * @throws WireException * */ public function ___delete(Saveable $item) { $blank = $this->makeBlankItem(); if(!$item instanceof $blank) { $typeName = $blank->className(); throw new WireException("WireSaveableItems::delete(item) requires item to be of type '$typeName'"); } $id = (int) $item->id; if(!$id) return false; $database = $this->wire()->database; $this->deleteReady($item); $this->getWireArray()->remove($item); $table = $database->escapeTable($this->getTable()); $query = $database->prepare("DELETE FROM `$table` WHERE id=:id LIMIT 1"); $query->bindValue(":id", $id, \PDO::PARAM_INT); $result = $query->execute(); if($result) { $this->deleted($item); $item->id = 0; } else { $this->error("Error deleting '$item'"); } return $result; } /** * Create and return a cloned copy of this item * * If no name is specified and the new item uses a 'name' field, it will contain a number at the end to make it unique * * @param Saveable $item Item to clone * @param string $name Optionally specify new name * @return bool|Saveable $item Returns the new clone on success, or false on failure * */ public function ___clone(Saveable $item, $name = '') { $original = $item; $item = clone $item; if(array_key_exists('name', $item->getTableData())) { // this item uses a 'name' field for identification, so we want to ensure it's unique $n = 0; if(!strlen($name)) $name = $item->name; // ensure the new name is unique while($this->get($name)) $name = rtrim($item->name, '_') . '_' . (++$n); $item->name = $name; } // id=0 forces the save() to create a new field $item->id = 0; $this->cloneReady($original, $item); if($this->save($item)) { $this->cloned($original, $item); return $item; } return false; } /** * Find items based on Selectors or selector string * * This is a delegation to the WireArray associated with this DAO. * This method assumes that all items are loaded. Desecending classes that don't load all items should * override this to the ___load() method instead. * * @param Selectors|string $selectors * @return WireArray * */ public function ___find($selectors) { if($this->useLazy()) $this->loadAllLazyItems(); return $this->getAll()->find($selectors); } #[\ReturnTypeWillChange] public function getIterator() { if($this->useLazy()) $this->loadAllLazyItems(); return $this->getAll(); } /** * Get an item * * @param string|int $key * @return array|mixed|null|Page|Saveable|Wire|WireData * */ public function get($key) { $value = $this->getWireArray()->get($key); if($value === null && $this->useLazy() && $key !== null) $value = $this->getLazy($key); return $value; } public function __get($name) { $value = $this->get($name); if($value === null) $value = parent::__get($name); return $value; } /** * Do we have the given item or item by given key? * * @param string|int|Saveable|WireData $item * @return bool * */ public function has($item) { if($this->useLazy() && !empty($this->lazyItems)) $this->get($item); // ensure lazy item present return $this->getAll()->has($item); } /** * Isset * * @param string|int $key * @return bool * */ public function __isset($key) { return $this->get($key) !== null; } /** * Get all property values for items * * This is useful for getting all property values without triggering lazy loaded items to load. * * #pw-internal * * @param string $valueType|array Name of property value you want to get, or array of them, i.e. 'id', 'name', etc. (default='id') * @param string $indexType One of 'name', 'id' or blank string for no index (default='') * @param string $matchType Optionally match this property, also requires $matchValue argument (default='') * @param string|int|array $matchValue Match this value for $matchType property, use array for OR values (default=null) * @return array * @since 3.0.194 * */ public function getAllValues($valueType = 'id', $indexType = '', $matchType = '', $matchValue = null) { $values = array(); $useValueArray = is_array($valueType); $matchArray = is_array($matchValue) ? array_flip($matchValue) : false; $items = $this->getWireArray(); if($this->useLazy()) { foreach($this->lazyItems as $row) { $index = null; if($matchValue !== null) { if($matchArray) { $v = isset($row[$matchType]) ? $row[$matchType] : null; if(!$v === null || !isset($matchArray[$v])) continue; } else { if($row[$matchType] != $matchValue) continue; } } if($indexType) { $index = isset($row[$indexType]) ? $row[$indexType] : $row['id']; } if($useValueArray) { /** @var array $valueType */ $value = array(); foreach($valueType as $key) { $value[$key] = isset($row[$key]) ? $row[$key] : null; } } else { $value = isset($row[$valueType]) ? $row[$valueType] : null; } if($index !== null) { $values[$index] = $value; } else { $values[] = $value; } } } foreach($items as $field) { /** @var WireData $field */ $index = null; if($matchValue !== null) { if($matchArray) { $v = $field->get($matchType); if($v === null || !isset($matchArray[$v])) continue; } else { if($field->get($matchType) != $matchValue) continue; } } if($indexType) { $index = $field->get($indexType); } if($useValueArray) { /** @var array $valueType */ $value = array(); foreach($valueType as $key) { $value[$key] = $field->get($key); } } else { $value = $field->get($valueType); } if($index !== null) { $values[$index] = $value; } else { $values[] = $value; } } return $values; } /** * Encode the 'data' portion of the table. * * This is a front-end to wireEncodeJSON so that it can be overridden if needed. * * @param array $value * @return string * */ protected function encodeData(array $value) { return wireEncodeJSON($value); } /** * Decode the 'data' portion of the table. * * This is a front-end to wireDecodeJSON that it can be overridden if needed. * * @param string $value * @return array * */ protected function decodeData($value) { return wireDecodeJSON($value); } /** * Enforce no locally-scoped fuel for this class * * @param bool|null $useFuel * @return bool * */ public function useFuel($useFuel = null) { return false; } /************************************************************************************** * HOOKERS * */ /** * Hook that runs right before item is to be saved. * * Unlike before(save), when this runs, it has already been confirmed that the item will indeed be saved. * * @param Saveable $item * */ public function ___saveReady(Saveable $item) { } /** * Hook that runs right before item is to be deleted. * * Unlike before(delete), when this runs, it has already been confirmed that the item will indeed be deleted. * * @param Saveable $item * */ public function ___deleteReady(Saveable $item) { } /** * Hook that runs right before item is to be cloned. * * @param Saveable $item * @param Saveable $copy * */ public function ___cloneReady(Saveable $item, Saveable $copy) { } /** * Hook that runs right before item is to be renamed. * * @param Saveable $item * @param string $oldName * @param string $newName * */ public function ___renameReady(Saveable $item, $oldName, $newName) { } /** * Hook that runs right after an item has been saved. * * Unlike after(save), when this runs, it has already been confirmed that the item has been saved (no need to error check). * * @param Saveable $item * @param array $changes * */ public function ___saved(Saveable $item, array $changes = array()) { if(count($changes)) { $this->log("Saved '$item->name', Changes: " . implode(', ', $changes)); } else { $this->log("Saved", $item); } } /** * Hook that runs right after a new item has been added. * * @param Saveable $item * */ public function ___added(Saveable $item) { $this->log("Added", $item); } /** * Hook that runs right after an item has been deleted. * * Unlike after(delete), it has already been confirmed that the item was indeed deleted. * * @param Saveable $item * */ public function ___deleted(Saveable $item) { $this->log("Deleted", $item); } /** * Hook that runs right after an item has been cloned. * * @param Saveable $item * @param Saveable $copy * */ public function ___cloned(Saveable $item, Saveable $copy) { $this->log("Cloned '$item->name' to '$copy->name'", $item); } /** * Hook that runs right after an item has been renamed. * * @param Saveable $item * @param string $oldName * @param string $newName * */ public function ___renamed(Saveable $item, $oldName, $newName) { $this->log("Renamed $oldName to $newName", $item); } /************************************************************************************** * OTHER * */ /** * Enables use of $apivar('name') or wire()->apivar('name') * * @param $key * @return Wire|null * */ public function __invoke($key) { return $this->get($key); } /** * Save to activity log, if enabled in config * * @param $str * @param Saveable|null Item to log * @return WireLog * */ public function log($str, Saveable $item = null) { $logs = $this->wire()->config->logs; $name = $this->className(array('lowercase' => true)); if($logs && in_array($name, $logs)) { if($item && strpos($str, "'$item->name'") === false) $str .= " '$item->name'"; return parent::___log($str, array('name' => $name)); } return parent::___log(); } /** * Record an error * * @param string $text * @param int|bool $flags See Notices::flags * @return Wire|WireSaveableItems * */ public function error($text, $flags = 0) { $this->log($text); return parent::error($text, $flags); } /** * debugInfo PHP 5.6+ magic method * * This is used when you print_r() an object instance. * * @return array * */ public function __debugInfo() { $info = array(); // parent::__debugInfo(); $info['loaded'] = array(); $info['notLoaded'] = array(); foreach($this->getWireArray() as $item) { /** @var WireData|Saveable $item */ $when = $item->get('_lazy'); $value = $item->get('name|id'); $value = $value ? "$value ($when)" : $item; $info['loaded'][] = $value; } foreach($this->lazyItems as $row) { $value = null; if(isset($row['name'])) $value = $row['name']; if(!$value && isset($row['id'])) $value = $row['id']; if(!$value) $value = &$row; $info['notLoaded'][] = $value; } return $info; } /************************************************************************************** * LAZY LOADING * */ /** * Lazy loaded raw item data from database * * @var array * */ protected $lazyItems = array(); // [ 0 => [ ... ], 1 => [ ... ], etc. ] protected $lazyNameIndex = array(); // [ 'name' => 123 ] where 123 is key in $lazyItems protected $lazyIdIndex = array(); // [ 3 => 123 ] where 3 is ID and 123 is key in $lazyItems /** * @var bool|null * */ protected $useLazy = null; /** * Use lazy loading for this type? * * @return bool * @since 3.0.194 * */ public function useLazy() { if($this->useLazy !== null) return $this->useLazy; $this->useLazy = $this->wire()->config->useLazyLoading; if(is_array($this->useLazy)) $this->useLazy = in_array(strtolower($this->className()), $this->useLazy); return $this->useLazy; } /** * Remove item from lazy loading data/indexes * * @param Saveable $item * @return bool * */ protected function unsetLazy(Saveable $item) { if(!isset($this->lazyIdIndex[$item->id])) return false; $key = $this->lazyIdIndex[$item->id]; unset($this->lazyItems[$key], $this->lazyNameIndex[$item->name], $this->lazyIdIndex[$item->id]); return true; } /** * Load all pending lazy-loaded items * * #pw-internal * */ public function loadAllLazyItems() { if(!$this->useLazy()) return; if(empty($this->lazyItems)) return; $debug = $this->wire()->config->debug; $items = $this->getWireArray(); $sortable = !empty($this->lazyNameIndex); foreach(array_keys($this->lazyItems) as $key) { if(!isset($this->lazyItems[$key])) continue; // required $row = &$this->lazyItems[$key]; $item = $this->initItem($row, $items); if($debug) $item->setQuietly('_lazy', '*'); } if($sortable) $items->sort('name'); // a-z $this->lazyItems = array(); $this->lazyNameIndex = array(); $this->lazyIdIndex = array(); // if you want to identify what triggered a “load all”, uncomment one of below: // bd(Debug::backtrace()); // $this->warning(Debug::backtrace()); } /** * Lazy load items by property value * * #pw-internal * * @param string $key i.e. fieldgroups_id * @param string|int $value * @todo I don't think we need this method, but leaving it here temporarily for reference * @deprecated * */ private function loadLazyItemsByValue($key, $value) { $debug = $this->wire()->config->debug; $items = $this->getWireArray(); foreach($this->lazyItems as $lazyKey => $lazyItem) { if($lazyItem[$key] != $value) continue; $item = $this->initItem($lazyItem, $items); unset($this->lazyItems[$lazyKey]); if($debug) $item->setQuietly('_lazy', '='); } } /** * Get a lazy loaded item, companion to get() method * * #pw-internal * * @param string|int $value * @return Saveable|Wire|WireData|null * @since 3.0.194 * */ protected function getLazy($value) { $property = ctype_digit("$value") ? 'id' : 'name'; $value = $property === 'id' ? (int) $value : "$value"; $item = null; $lazyItem = null; $lazyKey = null; if(!empty($this->lazyIdIndex)) { if($property === 'id') { $index = &$this->lazyIdIndex; } else { $index = &$this->lazyNameIndex; } if(isset($index[$value])) { $lazyKey = $index[$value]; $lazyItem = $this->lazyItems[$lazyKey]; } } else { foreach($this->lazyItems as $key => $row) { if(!isset($row[$property]) || $row[$property] != $value) continue; $lazyKey = $key; $lazyItem = $row; break; } } if($lazyItem) { $item = $this->initItem($lazyItem); $this->getWireArray()->add($item); unset($this->lazyItems[$lazyKey]); if($this->wire()->config->debug) $item->setQuietly('_lazy', '1'); } if($item === null && $property === 'name' && !ctype_alnum($value)) { if(Selectors::stringHasOperator("$value") || strpos("$value", '|')) { $this->loadAllLazyItems(); $item = $this->getWireArray()->get($value); } } return $item; } }