pages = $pages; } /** * Find pages and return raw data from them in a PHP array * * @param string|array|Selectors $selector * @param string|array|Field $field Name of field/property to get, or array of them, CSV string, or omit to get all (default='') * - Optionally use associative array to rename fields in returned value, i.e. `['title' => 'label']` returns 'title' as 'label' in return value. * - Specify `parent.field_name` or `parent.parent.field_name`, etc. to return values from parent(s). 3.0.193+ * - Specify `references` or `references.field_name`, etc. to also return values from pages referencing found pages. 3.0.193+ * - Specify `meta` or `meta.name` to also return values from page meta data. 3.0.193+ * @param array $options See options for Pages::find * - `objects` (bool): Use objects rather than associative arrays? (default=false) * - `entities` (bool|array): Entity encode string values? True, or specify array of field names. (default=false) * - `nulls` (bool): Populate nulls for field values that are not present, rather than omitting them? (default=false) 3.0.198+ * - `indexed` (bool): Index by page ID? (default=true) * - `flat` (bool|string): Flatten return value as `["field.subfield" => "value"]` rather than `["field" => ["subfield" => "value"]]`? * Optionally specify field delimiter, otherwise a period `.` will be used as the delimiter. (default=false) 3.0.193+ * - Note the `objects` and `flat` options are not meant to be used together. * * @return array * @since 3.0.172 * */ public function find($selector, $field = '', $options = array()) { if(!is_array($options)) $options = array('indexed' => (bool) $options); $finder = new PagesRawFinder($this->pages); $this->wire($finder); return $finder->find($selector, $field, $options); } /** * Get page (no exclusions) and return raw data from it in a PHP array * * @param string|array|Selectors $selector * @param string|Field|int|array $field Field/property name to get or array of them (or omit to get all) * @param array|bool $options See options for Pages::find * - `objects` (bool): Use objects rather than associative arrays? (default=false) * - `entities` (bool|array): Entity encode string values? True, or specify array of field names. (default=false) * - `indexed` (bool): Index by page ID? (default=false) * - `flat` (bool|string): Flatten return value as `["field.subfield" => "value"]` rather than `["field" => ["subfield" => "value"]]`? * Optionally specify field delimiter, otherwise a period `.` will be used as the delimiter. (default=false) 3.0.193+ * @return array * @since 3.0.172 * */ public function get($selector, $field = '', $options = array()) { if(!is_array($options)) $options = array('indexed' => (bool) $options); $options['findOne'] = true; if(!isset($options['findAll'])) $options['findAll'] = true; $values = $this->find($selector, $field, $options); return reset($values); } /** * Get native pages table column value for given page ID * * This can only be used for native 'pages' table columns, * i.e. id, name, templates_id, status, parent_id, etc. * * @param int|array $pageId Page ID or array of page IDs * @param string|array $col Column name you want to get * @return int|string|array|null Returns column value or array of column values if $pageId was an array. * When array is returned, it is indexed by page ID. * @param array $options * - `cache` (bool): Allow use of memory cache to retrieve column value when available? (default=true) * Used only if $pageId is an integer (not used when array of page IDs). * @throws WireException * @since 3.0.190 * * */ public function col($pageId, $col, array $options = array()) { $defaults = array( 'cache' => true ); $options = array_merge($defaults, $options); // delegate to cols() method when arguments require it if(is_array($col)) { return $this->cols($pageId, $col, $options); } else if(is_array($pageId)) { $value = array(); foreach($this->cols($pageId, $col) as $id => $a) { $value[$id] = $a[$col]; } return $value; } if(!ctype_alnum($col)) { $sanitizer = $this->wire()->sanitizer; if($sanitizer->fieldName($col) !== $col) { throw new WireException("Invalid column name: $col"); } } $pageId = (int) $pageId; // use cached value when available if($options['cache']) { $page = $this->pages->cacher()->getCache($pageId); if($page) return $page->getUnformatted($col); } $database = $this->wire()->database; $col = $database->escapeCol($col); $query = $database->prepare("SELECT `$col` FROM pages WHERE id=:id"); $query->bindValue(':id', $pageId, (int) \PDO::PARAM_INT); $query->execute(); $value = $query->rowCount() ? $query->fetchColumn() : null; $query->closeCursor(); return $value; } /** * Get native pages table columns (plural) for given page ID * * This can only be used for native 'pages' table columns, * i.e. id, name, templates_id, status, parent_id, etc. * * @param int|array $pageId Page ID or array of page IDs * @param array|string $cols Names of columns to get or omit to get all columns * @param array $options * - `cache` (bool): Allow use of memory cache to retrieve column value when available? (default=true) * Used only if $pageId is an integer (not used when array of page IDs). * @return array Returns associative array on success or empty array if not found * If $pageId argument was an array then it returns a page ID indexed array of * associative arrays, one for each page. * @throws WireException * @since 3.0.190 * */ public function cols($pageId, $cols = array(), array $options = array()) { $defaults = array( 'cache' => true, ); $options = array_merge($defaults, $options); $sanitizer = $this->wire()->sanitizer; $database = $this->wire()->database; $query = null; $removeIdInReturn = false; if(!is_array($cols)) $cols = empty($cols) ? array() : array($cols); foreach($cols as $key => $col) { if(!ctype_alnum($col) && $sanitizer->fieldName($col) !== $col) { unset($cols[$key]); } else { $cols[$key] = $database->escapeCol($col); } } if(count($cols)) { $colStr = '`' . implode('`,`', $cols) . '`'; if(is_array($pageId) && !in_array('id', $cols)) { $colStr .= ', id'; $removeIdInReturn = true; } } else { $colStr = '*'; } if(is_array($pageId)) { // multi page $ids = array(); foreach($pageId as $id) { $id = (int) $id; if($id > 0) $ids[$id] = $id; } $ids = implode(',', $ids); $query = $database->prepare("SELECT $colStr FROM pages WHERE id IN($ids)"); $query->execute(); $value = array(); while($row = $query->fetch(\PDO::FETCH_ASSOC)) { $id = (int) $row['id']; if($removeIdInReturn) unset($row['id']); foreach($row as $k => $v) { if(ctype_digit("$v")) $row[$k] = (int) $v; } $value[$id] = $row; } } else { // single page $pageId = (int) $pageId; $page = ($options['cache'] ? $this->pages->cacher()->getCache($pageId) : null); if($page) { $value = array(); foreach($cols as $col) { $value[$col] = $page->get($col); } } else { $query = $database->prepare("SELECT $colStr FROM pages WHERE id=:id"); $query->bindValue(':id', $pageId, (int) \PDO::PARAM_INT); $query->execute(); $value = $query->rowCount() ? $query->fetch(\PDO::FETCH_ASSOC) : array(); } } if($query) $query->closeCursor(); return $value; } } /** * ProcessWire Pages Raw Finder * */ class PagesRawFinder extends Wire { /** * @var Pages * */ protected $pages; /** * @var array * */ protected $options = array(); /** * @var array * */ protected $defaults = array( 'indexed' => true, 'objects' => false, 'entities' => false, 'nulls' => false, 'findOne' => false, 'flat' => false, ); /** * @var string|array|Selectors * */ protected $selector = ''; /** * @var bool * */ protected $selectorIsPageIDs = false; /** * @var array * */ protected $requestFields = array(); /** * @var array * */ protected $nativeFields = array(); /** * @var array * */ protected $parentFields = array(); /** * @var array * */ protected $childrenFields = array(); /** * @var array * */ protected $templateFields = array(); /** * @var array * */ protected $customFields = array(); /** * @var array * */ protected $runtimeFields = array(); /** * Fields to rename in returned value, i.e. [ 'title' => 'label' ] * * @var array * */ protected $renameFields = array(); /** * Temporary fields set to $this->value that should be unset from return value * * @var array * */ protected $unsetFields = array(); /** * @var array * */ protected $customCols = array(); /** * Columns requested as fieldName.col rather than fieldName[col] * * (not currently accounted for, future use) * * @var array * */ protected $customDotCols = array(); /** * Results of the raw find * * @var array * */ protected $values = array(); /** * True to return array indexed by field name for each page, false to return single value for each page * * @var bool * */ protected $getMultiple = true; /** * Get all data for pages? * * @var bool * */ protected $getAll = false; /** * Get/join the pages_paths table? * * @var bool * */ protected $getPaths = false; /** * IDs of pages to find, becomes array once known * * @var null|array|string * */ protected $ids = null; /** * Construct * * @param Pages $pages * */ public function __construct(Pages $pages) { parent::__construct(); $this->pages = $pages; } /** * @param string|int|array|Selectors * @param string|array|Field $field * @param array $options * */ protected function init($selector, $field, $options) { $fields = $this->wire()->fields; $selectorString = ''; $this->selector = $selector; $this->options = array_merge($this->defaults, $options); $this->values = array(); $this->requestFields = array(); $this->customFields = array(); $this->nativeFields = array(); $this->customCols = array(); $this->getMultiple = true; $this->getAll = false; $this->ids = null; if(is_array($selector)) { $val = reset($selector); $key = key($selector); if(ctype_digit("$key") && !is_array($val) && ctype_digit("$val")) $this->selectorIsPageIDs = true; } else { $selectorString = (string) $selector; } if(empty($field) && !$this->selectorIsPageIDs) { // check if field specified in selector instead $field = array(); $multi = false; if(!$selector instanceof Selectors) { $selector = new Selectors($selector); $this->wire($selector); } foreach($selector as $item) { if(!$item instanceof SelectorEqual) continue; $name = $item->field(); if($name !== 'field' && $name !== 'join' && $name !== 'fields') continue; if($name !== 'fields' && $fields->get($name)) continue; $value = $item->value; if(is_array($value)) { $field = array_merge($field, $value); } else if($value === 'all') { $this->getAll = true; } else { $field[] = $value; } $selector->remove($item); if($name === 'fields') $multi = true; } $this->selector = $selector; if(!$multi && count($field) === 1) $field = reset($field); } if(empty($field)) { $this->getAll = true; } else if(is_string($field) && strpos($field, ',') !== false) { // multiple fields requested in CSV string, we will return an array for each page $this->requestFields = explode(',', $field); foreach($this->requestFields as $k => $v) { $this->requestFields[$k] = trim($v); } } else if(is_array($field)) { // one or more fields requested in array, we will return an array for each page $this->requestFields = array(); $this->renameFields = array(); $this->processRequestFieldsArray($field); } else { // one field requested in string or Field object $this->requestFields = array($field); $this->getMultiple = false; } if($this->getAll) { if($this->wire()->modules->isInstalled('PagePaths')) { $this->getPaths = true; $this->runtimeFields['url'] = 'url'; $this->runtimeFields['path'] = 'path'; } } else { // split request fields into nativeFields and customFields $this->splitFields(); } // detect options in selector $optionsValues = array(); foreach(array('objects', 'entities', 'flat', 'nulls', 'options') as $name) { if($this->selectorIsPageIDs) continue; if($selectorString && strpos($selectorString, "$name=") === false) continue; if($fields->get($name)) continue; // if maps to a real field then ignore $result = Selectors::selectorHasField($this->selector, $name, array( 'operator' => '=', 'verbose' => true, 'remove' => true, )); $value = $result['value']; if($result['result'] && $value && !isset($options[$name])) { if($name === 'options') { if(is_string($value)) $optionsValues[] = $value; if(is_array($value)) $optionsValues = array_merge($optionsValues, $value); } else if(is_array($value)) { $this->options[$name] = array(); foreach($value as $v) $this->options[$name][$v] = $v; } else if(!ctype_digit("$value")) { $this->options[$name] = $value; } else { $this->options[$name] = (bool) ((int) $value); } } if(!empty($result['selectors'])) $this->selector = $result['selectors']; } foreach(array('objects', 'entities', 'flat') as $name) { if(in_array($name, $optionsValues)) $this->options[$name] = true; } } /** * Find pages and return raw data from them in a PHP array * * How to use the `$field` argument: * * - If you provide an array for $field then it will return an array for each page, indexed by * the field names you requested. * * - If you provide a string (field name) or Field object, then it will return an array with * the values of the 'data' column of that field. * * - You may request field name(s) like `field.subfield` to retrieve a specific column/subfield. * * - You may request field name(s) like `field.*` to return all columns/subfields for `field`, * in this case, an associative array value will be returned for each page. * * - If you specify an associative array for the $field argument, you can optionally rename * fields in returned value. For example, if you wanted to get the 'title' field but return * it as a field named 'headline' in the return value, you would specify the array * `[ 'title' => 'headline' ]` for the $field argument. (3.0.176+) * * @param string|array|Selectors $selector * @param string|Field|int|array $field Field/property name or array of of them * @param array $options See options for Pages::find * @return array * @since 3.0.172 * */ public function find($selector, $field = '', $options = array()) { static $level = 0; $level++; $this->init($selector, $field, $options); if(count($this->parentFields) && !isset($this->nativeFields['parent_id'])) { // we need parent_id if finding any parent fields $this->nativeFields['parent_id'] = 'parent_id'; $this->unsetFields['parent_id'] = 'parent_id'; } if(count($this->templateFields) && !isset($this->nativeFields['templates_id'])) { // we need templates_id if finding any template properties $this->nativeFields['templates_id'] = 'templates_id'; $this->unsetFields['templates_id'] = 'templates_id'; } // requested native pages table fields/properties if(count($this->nativeFields) || $this->getAll || $this->getPaths) { // one or more native pages table column(s) requested $this->findNativeFields(); } // requested custom fields if(count($this->customFields) || $this->getAll) { $this->findCustom(); } // requested runtime fields if(count($this->runtimeFields)) { $this->findRuntime(); } // requested parent fields if(count($this->parentFields)) { $this->findParent(); } // requested template fields if(count($this->templateFields)) { $this->findTemplate(); } // remove runtime only fields if(count($this->unsetFields)) { foreach($this->unsetFields as $name) { foreach($this->values as $key => $value) { unset($this->values[$key][$name]); } } } // reduce return value when expected if(!$this->getMultiple) { foreach($this->values as $id => $row) { $this->values[$id] = reset($row); } } if(!$this->options['indexed']) { $this->values = array_values($this->values); } if(count($this->renameFields)) { $this->renames($this->values); } if($this->options['entities']) { if($this->options['objects'] || $level === 1) { $this->entities($this->values); } } if($this->options['flat']) { $delimiter = is_string($this->options['flat']) ? $this->options['flat'] : '.'; foreach($this->values as $key => $value) { if(is_array($value)) { $this->values[$key] = $this->flattenValues($value, '', $delimiter); } } } if($this->options['nulls']) { $this->populateNullValues($this->values); } if($this->options['objects']) { $this->objects($this->values); } $level--; return $this->values; } /** * Split requestFields into native and custom field arrays * * Populates $this->nativeFields, $this->customFields, $this->customCols * */ protected function splitFields() { $fields = $this->wire()->fields; $fails = array(); $runtimeNames = array('meta', 'references'); // split request fields into custom fields and native (pages table) fields foreach($this->requestFields as $key => $fieldName) { if(empty($fieldName)) continue; if(is_string($fieldName)) $fieldName = trim($fieldName); $colName = ''; $dotCol = false; $fieldObject = null; $fullName = $fieldName; if($fieldName === '*') { // get all (not yet supported) } else if($fieldName instanceof Field) { // Field object $fieldObject = $fieldName; } else if(is_array($fieldName)) { // Array where [ 'field' => [ 'subfield' ]] $colName = $fieldName; // array $fieldName = $key; if($fieldName === 'parent' || $fieldName === 'children' || $fieldName === 'template') { // passthru } else if(in_array($fieldName, $runtimeNames) && !$fields->get($fieldName)) { // passthru $this->runtimeFields[$fullName] = $fullName; continue; } else { $fieldObject = isset($this->customFields[$fieldName]) ? $this->customFields[$fieldName] : null; if(!$fieldObject) $fieldObject = $fields->get($fieldName); if(!$fieldObject) continue; } } else if(is_int($fieldName) || ctype_digit("$fieldName")) { // Field ID $fieldObject = $fields->get((int) $fieldName); } else if(is_string($fieldName)) { // Field name, subfield/column may optionally be specified as field.subfield if(strpos($fieldName, '.')) { list($fieldName, $colName) = explode('.', $fieldName, 2); $dotCol = true; } else if(strpos($fieldName, '[')) { list($fieldName, $colName) = explode('[', $fieldName, 2); $colName = rtrim($colName, ']'); } if($fieldName === 'parent' || $fieldName === 'children' || $fieldName === 'template') { // passthru } else if(in_array($fieldName, $runtimeNames) && !$fields->get($fieldName)) { // passthru $this->runtimeFields[$fullName] = $fullName; continue; } else { $fieldObject = isset($this->customFields[$fieldName]) ? $this->customFields[$fieldName] : null; if(!$fieldObject) $fieldObject = $fields->get($fieldName); } } else { // something we do not recognize $fails[] = $fieldName; continue; } if($fieldName === 'parent') { $this->parentFields[$fullName] = $colName; } else if($fieldName === 'children') { // @todo not yet supported $this->childrenFields[$fullName] = $colName; } else if($fieldName === 'template') { $this->templateFields[$fullName] = $colName; } else if($fullName === 'url' || $fullName === 'path') { if($this->wire()->modules->isInstalled('PagePaths')) { $this->runtimeFields[$fullName] = $fullName; $this->getPaths = true; } else { $fails[] = "Property '$fullName' requires the PagePaths module be installed"; } } else if($fieldObject instanceof Field) { $this->customFields[$fieldName] = $fieldObject; if(!empty($colName)) { $colNames = is_array($colName) ? $colName : array($colName); foreach($colNames as $col) { if(!isset($this->customCols[$fieldName])) $this->customCols[$fieldName] = array(); $this->customCols[$fieldName][$col] = $col; if($dotCol) { if(!isset($this->customDotCols[$fieldName])) $this->customDotCols[$fieldName] = array(); $this->customDotCols[$fieldName][$col] = $col; } } } } else { $this->nativeFields[$fieldName] = $fieldName; } } if(count($fails)) $this->unknownFieldsException($fails); } /** * Find raw native fields * */ protected function findNativeFields() { $this->ids = array(); $allNatives = array(); $fails = array(); $rootUrl = $this->wire()->config->urls->root; $templates = $this->wire()->templates; $templatesById = array(); $getPaths = $this->getPaths; if(empty($this->selector)) return; foreach($this->findIDs($this->selector, '*') as $row) { $id = (int) $row['id']; $this->ids[$id] = $id; $this->values[$id] = isset($this->values[$id]) ? array_merge($this->values[$id], $row) : $row; if(empty($allNatives)) { foreach(array_keys($row) as $key) { $allNatives[$key] = $key; } } } if(!count($this->values)) return; if($this->getAll) $this->nativeFields = $allNatives; // native columns we will populate into $values $getNatives = array(); foreach($this->nativeFields as $fieldName) { if($fieldName === '*' || $fieldName === 'pages' || $fieldName === 'pages.*') { // get all columns $colName = ''; } else if(strpos($fieldName, 'pages.') === 0) { // pages table column requested by name list(,$colName) = explode('.', $fieldName, 2); } else { // column requested by name on its own $colName = $fieldName; } if(empty($colName)) { // get all native pages table columns $getNatives = $allNatives; } else if(isset($allNatives[$colName])) { // get specific native pages table columns $getNatives[$colName] = $colName; } else { // fieldName is not a known field or pages column $fails[] = "$fieldName"; } } if(count($fails)) $this->unknownFieldsException($fails, 'column/field'); if(!count($getNatives) && !$getPaths) return; // remove any native data that is present but was not requested and populate any runtime fields foreach($this->values as $id => $row) { $templateId = (int) $row['templates_id']; foreach($row as $colName => $value) { if($getPaths && $colName === 'path') { // populate path and/or url runtime properties if(!isset($templatesById[$templateId])) $templatesById[$templateId] = $templates->get($templateId); $template = $templatesById[$templateId]; /** @var Template $template */ $slash = $template->slashUrls ? '/' : ''; $path = strlen($value) && $value !== '/' ? "$value$slash" : ''; if(isset($this->runtimeFields['url'])) { $this->values[$id]['url'] = $rootUrl . $path; } if(isset($this->runtimeFields['path'])) { $this->values[$id]['path'] = "/$path"; } else { unset($this->values[$id]['path']); } } else if(!isset($getNatives[$colName])) { unset($this->values[$id][$colName]); } } } } /** * Gateway to finding custom fields whether specific, all or none * */ protected function findCustom() { if(count($this->customFields)) { // one or more custom fields requested if($this->ids === null) { // only find IDs if we didn’t already in the nativeFields section $this->setIds($this->findIDs($this->selector, false)); } if(!count($this->ids)) return; foreach($this->customFields as $fieldName => $field) { /** @var Field $field */ $cols = isset($this->customCols[$fieldName]) ? $this->customCols[$fieldName] : array(); $this->findCustomField($field, $cols); } } else if($this->getAll && !empty($this->ids)) { $this->findCustomAll(); } } /** * Find raw custom field * * @param Field $field * @param array $cols * @throws WireException * */ protected function findCustomField(Field $field, array $cols) { $database = $this->wire()->database; $sanitizer = $this->wire()->sanitizer; $getArray = true; $getCols = array(); $skipCols = array(); $getAllCols = false; $getExternal = false; // true when request includes columns not in field’s DB schema $pageRefCols = array(); $externalCols = array(); // columns that are external from field’s DB schema /** @var FieldtypeMulti $fieldtypeMulti */ $fieldtype = $field->type; $fieldtypeMulti = $field->type instanceof FieldtypeMulti ? $fieldtype : null; $fieldtypePage = $fieldtype instanceof FieldtypePage ? $fieldtype : null; $fieldtypeRepeater = $fieldtype instanceof FieldtypeRepeater ? $fieldtype : null; $fieldName = $field->name; $schema = $fieldtype->getDatabaseSchema($field); $schema = $fieldtype->trimDatabaseSchema($schema, array('trimDefault' => false)); $table = $database->escapeTable($field->getTable()); $sorts = array(); if(empty($table) || empty($schema) || $fieldtype instanceof FieldtypeFieldsetOpen) return; if(empty($cols)) { // no cols specified $trimSchema = $fieldtype->trimDatabaseSchema($schema, array('trimDefault' => true, 'trimMeta' => true)); unset($trimSchema['data']); foreach($trimSchema as $key => $value) { // multi-language columns do not count as custom schema if(strpos($key, 'data') === 0 && ctype_digit(substr($key, 4))) unset($trimSchema[$key]); } if(empty($trimSchema)) { // if table doesn’t maintain a custom schema, just get data column $getArray = false; $getCols[] = 'data'; } else { // table maintains custom schema, get all columns $getAllCols = true; $skipCols[] = 'pages_id'; } } else if(reset($cols) === '*') { $getAllCols = true; if(wireInstanceOf($field->type, 'FieldtypeOptions')) $getExternal = true; } else { foreach($cols as $col) { $col = $sanitizer->name($col); if(empty($col)) continue; if(isset($schema[$col])) { $getCols[$col] = $database->escapeCol($sanitizer->fieldName($col)); } else if($fieldtypePage || $fieldtypeRepeater) { $pageRefCols[$col] = $col; } else { // unknown or external column $getCols['data'] = 'data'; $externalCols[$col] = $col; $getExternal = true; } } if(count($pageRefCols)) { // get just the data column when a field within a Page reference is asked for $getCols['data'] = 'data'; } if(count($getCols) === 1 && !$this->getMultiple && count($externalCols) < 2) { // if only getting single field we will populate its value rather than // its value in an associative array $getArray = false; } } if($fieldtypeMulti) { $orderByCols = $fieldtypeMulti->get('orderByCols'); if($fieldtypeMulti->useOrderByCols && !empty($orderByCols)) { foreach($orderByCols as $key => $col) { $desc = strpos($col, '-') === 0 ? ' DESC' : ''; $col = $sanitizer->fieldName(ltrim($col, '-')); if(!array_key_exists($col, $schema)) continue; $sorts[$key] = '`' . $database->escapeCol($col) . '`' . $desc; } } if(empty($sorts) && isset($schema['sort'])) { $sorts[] = "`sort`"; } } $this->ids(true); // converts this->ids to CSV string $idsCSV = &$this->ids; if(empty($idsCSV)) return; $colSQL = $getAllCols ? '*' : '`' . implode('`,`', $getCols) . '`'; if(!$getAllCols && !in_array('pages_id', $getCols)) $colSQL .= ',`pages_id`'; $orderby = array(); if(!count($this->nativeFields)) $orderby[] = "FIELD(pages_id, $idsCSV)"; if(count($sorts)) $orderby[] = implode(',', $sorts); $sql = "SELECT $colSQL FROM `$table` WHERE pages_id IN($idsCSV) "; if(count($orderby)) $sql .= "ORDER BY " . implode(',', $orderby); $query = $database->prepare($sql); $query->execute(); while($row = $query->fetch(\PDO::FETCH_ASSOC)) { $id = $row['pages_id']; if(!$getAllCols && !isset($getCols['pages_id'])) unset($row['pages_id']); foreach($skipCols as $skipCol) { unset($row[$skipCol]); } if($getAllCols) { $value = $row; } else if($getArray) { $value = array(); foreach($getCols as $col) { $value[$col] = isset($row[$col]) ? $row[$col] : null; } } else { $col = reset($getCols); if(empty($col)) $col = 'data'; $value = $row[$col]; } if(!isset($this->values[$id])) { // Overall page placeholder array $this->values[$id] = array(); } if($fieldtypeMulti) { // FieldtypeMulti types may contain multiple rows /** @var FieldtypeMulti $fieldtype */ if(!isset($this->values[$id][$fieldName])) { $this->values[$id][$fieldName] = array(); } if($fieldtypePage && count($pageRefCols)) { // reduce page reference to just the IDs, indexed by IDs if(isset($value['data'])) $value = $value['data']; $this->values[$id][$fieldName][$value] = $value; } else { $this->values[$id][$fieldName][] = $value; } } else if($fieldtypeRepeater && count($pageRefCols)) { $repeaterIds = isset($value['data']) ? explode(',', $value['data']) : explode(',', $value); foreach($repeaterIds as $repeaterId) { $this->values[$id][$fieldName][$repeaterId] = $repeaterId; } } else { $this->values[$id][$fieldName] = $value; } } $query->closeCursor(); if(count($pageRefCols)) { if($fieldtypePage) { $this->findCustomFieldtypePage($field, $fieldName, $pageRefCols); } else if($fieldtypeRepeater) { $this->findCustomFieldtypePage($field, $fieldName, $pageRefCols); } } if($getExternal) { if(wireInstanceOf($fieldtype, 'FieldtypeOptions')) { $this->findCustomFieldtypeOptions($field, $externalCols, $getArray, $getAllCols); } } } /** * Find custom Options fieldtype columns * * Options field stores its values/titles in separate table. * * To use, specify one of the following in the fields to get (where field_name is an options field name): * * - `field_name` to just include the IDs of the selected options for each page. * - `field_name.*` to include all available properties for selected options for each page. * - `field_name.title` to include the selected option titles. * - `field_name.value` to include the selected option values. * * @param Field $field * @param array $cols * @param bool $getArray * @param bool $getAllCols * @since 3.0.193 * */ protected function findCustomFieldtypeOptions(Field $field, $cols, $getArray, $getAllCols) { /** @var FieldtypeOptions $fieldtype */ $fieldtype = $field->type; $fieldName = $field->name; $options = $fieldtype->getOptions($field); $firstColName = reset($cols); foreach($this->values as $pageId => $data) { if(!isset($data[$fieldName])) continue; foreach($data[$fieldName] as $key => $optionValue) { if(is_array($optionValue)) { $optionId = (int) $optionValue['data']; } else if(ctype_digit("$optionValue")) { $optionId = (int) $optionValue; } else { continue; // not likely } /** @var SelectableOption $option */ $option = $options->get((int) $optionId); if(!$option) { // unknown option } else if($getAllCols) { $a = $option->getArray(); unset($a['sort']); $this->values[$pageId][$fieldName][$key] = $a; } else if($getArray) { $this->values[$pageId][$fieldName][$key] = array(); foreach($cols as $colName) { $value = $option->get($colName); if(!is_string($value) && !is_int($value)) $value = null; $this->values[$pageId][$fieldName][$key][$colName] = $option->get($colName); } } else { $value = $option->get($firstColName); // i.e. title, value, id, title1234, etc. $this->values[$pageId][$fieldName][$key] = (is_string($value) || is_int($value) ? $value : null); } } } } /** * Find and apply values for Page reference fields * * @param Field $field * @param string $fieldName * @param array $pageRefCols * */ protected function findCustomFieldtypePage(Field $field, $fieldName, array $pageRefCols) { $pageRefIds = array(); foreach($this->values as /* $pageId => */ $row) { if(!isset($row[$fieldName])) continue; $pageRefIds = array_merge($pageRefIds, $row[$fieldName]); } if(!$this->getMultiple && count($pageRefCols) === 1) { $pageRefCols = implode('', $pageRefCols); } $pageRefIds = array_unique($pageRefIds); $finder = new PagesRawFinder($this->pages); $this->wire($finder); $options = $this->options; $options['indexed'] = true; $pageRefRows = $finder->find($pageRefIds, $pageRefCols, $options); foreach($this->values as $pageId => $pageRow) { if(!isset($pageRow[$fieldName])) continue; foreach($pageRow[$fieldName] as $pageRefId) { $this->values[$pageId][$fieldName][$pageRefId] = $pageRefRows[$pageRefId]; } if(!$this->getMultiple && $field->get('derefAsPage') > 0) { $this->values[$pageId][$fieldName] = reset($this->values[$pageId][$fieldName]); } else if(empty($this->options['indexed'])) { $this->values[$pageId][$fieldName] = array_values($this->values[$pageId][$fieldName]); } } } /** * Find/populate all custom fields * */ protected function findCustomAll() { $idsByTemplate = array(); foreach($this->ids() as $id) { if(!isset($this->values[$id])) continue; $row = $this->values[$id]; $templateId = $row['templates_id']; if(!isset($idsByTemplate[$templateId])) $idsByTemplate[$templateId] = array(); $idsByTemplate[$templateId][$id] = $id; } foreach($idsByTemplate as $templateId => $pageIds) { $template = $this->wire()->templates->get($templateId); if(!$template) continue; foreach($template->fieldgroup as $field) { $this->findCustomField($field, array()); } } } /** * Find and apply values for parent.[field] * * @since 3.0.193 * */ protected function findParent() { $ids = array(); foreach($this->values as $pageId => $data) { $parentId = $data['parent_id']; if(!isset($ids[$parentId])) $ids[$parentId] = array(); $ids[$parentId][] = $pageId; } $finder = new PagesRawFinder($this->pages); $this->wire($finder); $options = $this->options; $options['indexed'] = true; $parentFields = array_values($this->parentFields); if(!$this->getMultiple && count($parentFields) < 2) { $parentFields = reset($parentFields); } $rows = $finder->find(array_keys($ids), $parentFields, $options); foreach($rows as $parentId => $row) { foreach($ids[$parentId] as $pageId) { $this->values[$pageId]['parent'] = $row; } } } /** * Find and apply values for template.[property] * * @since 3.0.206 * */ protected function findTemplate() { $templates = $this->wire()->templates; $templateFields = $this->templateFields; $templateData = array(); $templateIds = array(); foreach($this->values as /* $pageId => */ $data) { $templateId = $data['templates_id']; if(!isset($templateIds[$templateId])) $templateIds[$templateId] = $templateId; } foreach($templateIds as $templateId) { $template = $templates->get($templateId); $templateData[$templateId] = array(); foreach($templateFields as /* $fullName => */ $colName) { if(empty($colName)) $colName = 'name'; $value = $template->get($colName); if(is_object($value)) continue; // object values not allowed here $templateData[$templateId][$colName] = $value; } } if(!$this->getMultiple && count($this->templateFields) < 2) { $colName = reset($this->templateFields); foreach($templateData as $templateId => $data) { $templateData[$templateId] = $data[$colName]; } } foreach($this->values as $pageId => $data) { $templateId = $data['templates_id']; $this->values[$pageId]['template'] = $templateData[$templateId]; } } /** * Find runtime generated fields * * @since 3.0.193 * */ protected function findRuntime() { $runtimeFields = array(); $fieldNames = $this->runtimeFields; unset($fieldNames['url'], $fieldNames['path']); if(empty($fieldNames)) return; if($this->ids === null) { $this->setIds($this->findIDs($this->selector, false)); } foreach($fieldNames as $fieldName) { $colName = ''; if(strpos($fieldName, '.')) list($fieldName, $colName) = explode('.', $fieldName, 2); if(!isset($runtimeFields[$fieldName])) $runtimeFields[$fieldName] = array(); if($colName) $runtimeFields[$fieldName][] = $colName; } if(isset($runtimeFields['meta'])) { $this->findMeta($runtimeFields['meta']); } if(isset($runtimeFields['references'])) { $this->findReferences($runtimeFields['references']); } } /** * Populate 'meta' to (form pages_meta table) to the result values * * @param array $names * @since 3.0.193 * */ protected function findMeta(array $names) { if(empty($this->ids)) return; $this->ids(true); $getAll = $this->getAll || in_array('*', $names, true) || empty($names); if($getAll) $names = array(); $sql = "SELECT source_id, name, data FROM pages_meta WHERE source_id IN($this->ids)"; $query = $this->wire()->database->prepare($sql); $query->execute(); while($row = $query->fetch(\PDO::FETCH_ASSOC)) { $id = (int) $row['source_id']; $name = $row['name']; $data = json_decode($row['data'], true); if(!isset($this->values[$id]['meta'])) $this->values[$id]['meta'] = array(); if($getAll || in_array($name, $names, true)) $this->values[$id]['meta'][$name] = $data; } foreach(array_keys($this->values) as $id) { if(!isset($this->values[$id]['meta'])) $this->values[$id]['meta'] = array(); } $query->closeCursor(); } /** * Populate a 'references' to the raw results that includes other pages referencing the found ones * * To use this specify `references` in the fields to return. Or, to get page references that are * indexed by field name, specify `references.field` instead. To get something more than the id * of page references, specify properties or fields as `references.field_name` replacing `field_name` * with a page property or field name, i.e. `references.title`. * * @param array $colNames * @since 3.0.193 * */ protected function findReferences(array $colNames) { $database = $this->wire()->database; $pageFields = array(); if(empty($this->ids)) return; foreach($this->wire()->fields as $field) { if($field->type instanceof FieldtypePage) $pageFields[$field->name] = $field; } if(empty($pageFields)) return; foreach($this->values as $id => $data) { $this->values[$id]['references'] = array(); } $showField = array_search('field', $colNames); if($showField !== false) { unset($colNames[$showField]); $showField = true; } $this->ids(true); $fromPageIds = array(); $findPageIds = array(); foreach($pageFields as $pageField) { $fieldName = $pageField->name; /** @var Field $pageField */ $table = $pageField->getTable(); $sql = "SELECT pages_id, data FROM $table WHERE data IN($this->ids)"; $query = $database->prepare($sql); $query->execute(); while($row = $query->fetch(\PDO::FETCH_NUM)) { $fromPageId = (int) $row[0]; // pages_id $toPageId = (int) $row[1]; // data if(!isset($fromPageIds[$toPageId])) $fromPageIds[$toPageId] = array(); if(!isset($fromPageIds[$toPageId][$fieldName])) $fromPageIds[$toPageId][$fieldName] = array(); $fromPageIds[$toPageId][$fieldName][] = $fromPageId; $findPageIds[] = $fromPageId; } $query->closeCursor(); } if(!count($findPageIds)) return; if(empty($colNames)) { // shortcut: we only need to include the ids foreach($this->values as $toPageId => $data) { if(!isset($fromPageIds[$toPageId])) continue; if($showField) { $references = $fromPageIds[$toPageId]; } else { $references = array(); foreach($fromPageIds[$toPageId] as /* $fieldName => */ $ids) { $references = array_merge($references, $ids); } } if(!$this->options['indexed']) $references = array_values($references); $this->values[$toPageId]['references'] = $references; } return; } // load properties/fields from found references $finder = new PagesRawFinder($this->pages); $this->wire($finder); $options = $this->options; $options['indexed'] = true; $colNames = $this->getMultiple || count($colNames) > 1 ? $colNames : reset($colNames); $rows = $finder->find($findPageIds, $colNames, $options); foreach($this->values as $toPageId => $data) { if(!isset($fromPageIds[$toPageId])) continue; foreach($fromPageIds[$toPageId] as $fieldName => $fromIds) { foreach($fromIds as $fromId) { if(!isset($rows[$fromId])) continue; $row = $rows[$fromId]; if($showField) { if(!isset($this->values[$toPageId]['references'][$fieldName])) { $this->values[$toPageId]['references'][$fieldName] = array(); } if($this->options['indexed']) { $this->values[$toPageId]['references'][$fieldName][$fromId] = $row; } else { $this->values[$toPageId]['references'][$fieldName][] = $row; } } else { if($this->options['indexed']) { $this->values[$toPageId]['references'][$fromId] = $row; } else { $this->values[$toPageId]['references'][] = $row; } } } } } } /** * Front-end to pages.findIDs that optionally accepts array of page IDs * * @param array|string|Selectors $selector * @param bool|string $verbose One of true, false, or '*' * @param array $options * @return array * @throws WireException * */ protected function findIDs($selector, $verbose, array $options = array()) { $options = array_merge($this->options, $options); $options['verbose'] = $verbose; $options['indexed'] = true; $options['joinPath'] = $this->getPaths; // if selector was just a page ID, return it in an id indexed array if(is_int($selector) || (is_string($selector) && ctype_digit($selector))) { $id = (int) $selector; return array($id => $id); } // if selector is not array of page IDs then let pages.findIDs handle it if(!is_array($selector) || !isset($selector[0]) || is_array($selector[0]) || !ctype_digit((string) $selector[0])) { return $this->pages->findIDs($selector, $options); } // at this point selector is an array of page IDs if(empty($verbose)) { // if selector already has what is needed and verbose data not needed, // then return it now, but make sure it is indexed by ID first $a = array(); foreach($selector as $id) $a[(int) $id] = (int) $id; return $a; } // convert selector to CSV string of page IDs $selector = implode(',', array_map('intval', $selector)); $selects = array(); $joins = array(); $wheres = array("id IN($selector)"); if($verbose === '*') { // get all columns $selects[] = 'pages.*'; } else { // get just base columns $selects = array('pages.id', 'pages.templates_id', 'pages.parent_id'); } if($this->getPaths) { $selects[] = 'pages_paths.path AS path'; $joins[] = 'LEFT JOIN pages_paths ON pages_paths.pages_id=pages.id'; } $sql = "SELECT " . implode(', ', $selects) . " " . "FROM pages " . (count($joins) ? implode(' ', $joins) . " " : '') . "WHERE " . implode(' ', $wheres); $query = $this->wire()->database->prepare($sql); $query->execute(); $rows = array(); while($row = $query->fetch(\PDO::FETCH_ASSOC)) { $id = (int) $row['id']; $rows[$id] = $row; } $query->closeCursor(); return $rows; } /** * Convert associative arrays to objects * * @param array $values * */ protected function objects(&$values) { foreach(array_keys($values) as $key) { $value = $values[$key]; if(!is_array($value)) continue; reset($value); if(is_int(key($value))) continue; $this->objects($value); $values[$key] = (object) $value; } } /** * Apply entity encoding to all strings in given value, recursively * * @param mixed $value * */ protected function entities(&$value) { $prefix = ''; // populate for testing only if(is_string($value)) { // entity-encode $value = $prefix . htmlentities($value, ENT_QUOTES, 'UTF-8'); } else if(is_array($value)) { // iterate and go recursive foreach(array_keys($value) as $key) { if(is_array($value[$key])) { $this->entities($value[$key]); } else if(is_string($value[$key])) { if($this->options['entities'] === true || $this->options['entities'] === $key || isset($this->options['entities'][$key])) { $value[$key] = $prefix . htmlentities($value[$key], ENT_QUOTES, 'UTF-8'); } } } } else { // leave as-is } } /** * Rename fields on request * * @param array $values * @since 3.0.167 * */ protected function renames(&$values) { foreach($values as $key => $value) { if(!is_array($value)) continue; foreach($value as $k => $v) { if(is_array($v)) $this->renames($v); if(isset($this->renameFields[$k])) { unset($values[$key][$k]); $name = $this->renameFields[$k]; } else { $name = $k; } $values[$key][$name] = $v; } } } /** * Get or convert $this->ids to/from CSV * * The point of this is just to minimize the quantity of copies of IDs we are keeping around. * In case the quantity gets to be huge, it'll be more memory friendly. * * @param bool $csv * @return array|string * */ protected function ids($csv = false) { if($this->ids === null) return $csv ? '' : array(); if($csv) { if(is_array($this->ids)) $this->ids = implode(',', array_map('intval', $this->ids)); } else if(is_string($this->ids)) { // this likely cannot occur with current logic but here in case that changes $this->ids = explode(',', $this->ids); } return $this->ids; } /** * Set the found IDs and init the $this->values array * * @param array $ids * @since 3.0.193 * */ protected function setIds(array $ids) { $this->ids = $ids; foreach($ids as $id) { $this->values[$id] = array(); } } /** * Flatten multidimensional values from array['a']['b']['c'] to array['a.b.c'] * * @param array $values * @param string $prefix Prefix for recursive use * @param string $delimiter * @return array * @since 3.0.193 * */ protected function flattenValues(array $values, $prefix = '', $delimiter = '.') { $flat = array(); foreach($values as $key => $value) { if(!is_array($value)) { if(ctype_digit("$key") && $prefix) { // integer keys map to array values $k = rtrim($prefix, $delimiter); if(!isset($flat[$k])) $flat[$k] = array(); if(!is_array($flat[$k])) $flat[$k] = array($flat[$k]); $flat[$k][$key] = $value; } else { $flat["$prefix$key"] = $value; } continue; } $a = $this->flattenValues($value, "$prefix$key$delimiter", $delimiter); if(!is_int($key)) { $flat = $flat + $a; continue; } $converted = false; // convert categories.1234.title => categories.title = array(1234 => 'title', ...); foreach($a as $k => $v) { if(strpos($k, "$delimiter$key$delimiter") === false) continue; list($k1, $k2) = explode("$delimiter$key$delimiter", $k); unset($a[$k]); $kk = "$k1$delimiter$k2"; if(!isset($flat[$kk])) $flat[$kk] = array(); $flat[$kk][$key] = $v; $converted = true; } if(!$converted) $flat = $flat + $a; } return $flat; } /** * Populate null values for requested fields that were not present (the 'nulls' option) * * Applies only if specific fields were requested. * * @var array $values * @since 3.0.198 * */ protected function populateNullValues(&$values) { $emptyValue = array(); if(count($this->requestFields)) { // specific fields requested foreach($this->requestFields as $name) { if(isset($this->renameFields[$name])) $name = $this->renameFields[$name]; if(!$this->options['flat'] && strpos($name, '.')) list($name,) = explode('.', $name, 2); $emptyValue[$name] = null; } foreach($values as $key => $value) { $values[$key] = array_merge($emptyValue, $value); } } else { // all fields requested $templates = $this->wire()->templates; $emptyValues = array(); foreach($values as $key => $value) { if(!isset($value['templates_id'])) continue; $tid = (int) $value['templates_id']; if(isset($emptyValues[$tid])) { $emptyValue = $emptyValues[$tid]; } else { $template = $templates->get((int) $value['templates_id']); if(!$template) continue; $emptyValue = array(); foreach($template->fieldgroup as $field) { $emptyValue[$field->name] = null; } $emptyValues[$tid] = $emptyValue; } $values[$key] = array_merge($emptyValue, $value); } } } /** * Process given array of values to populate $this->requestFields and $this->renameFields * * @param array $values * @param string $prefix Prefix for recursive use * @since 3.0.194 * */ protected function processRequestFieldsArray(array $values, $prefix = '') { if($prefix) $prefix = rtrim($prefix, '.') . '.'; foreach($values as $key => $value) { if(ctype_digit("$key")) { // i.e. [ 0 => 'field_name', 1 => 'another_field' ] if(is_string($value) && !ctype_digit("$value")) { $this->requestFields[] = $prefix . $value; } else { // error, not supported } } else if(is_array($value)) { // i.e. [ 'field_name' => [ 'id', 'title' ] $this->processRequestFieldsArray($value, $prefix . $key); } else { // rename i.e. [ 'field_name' => 'new_field_name' ] $this->requestFields[] = $prefix . $key; $this->renameFields[$prefix . $key] = $value; } } } protected function unknownFieldsException(array $fieldNames, $context = '') { if($context) $context = " $context"; $s = "Unknown$context name(s) for findRaw: " . implode(', ', $fieldNames); throw new WireException($s); } }