artabro/wire/core/PagesRaw.php
2024-08-27 11:35:37 +02:00

1764 lines
50 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php namespace ProcessWire;
/**
* ProcessWire Pages Raw Tools
*
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
* https://processwire.com
*
*/
class PagesRaw extends Wire {
/**
* @var Pages
*
*/
protected $pages;
/**
* Construct
*
* @param Pages $pages
*
*/
public function __construct(Pages $pages) {
parent::__construct();
$this->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 didnt 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 fields DB schema
$pageRefCols = array();
$externalCols = array(); // columns that are external from fields 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 doesnt 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);
}
}