mirror of
https://github.com/processwire/processwire.git
synced 2025-08-11 09:14:58 +02:00
Add support for filtered or paginated load of page reference fields per processwire/processwire-requests#13 ... note that paginated/filered save is not yet supported
This commit is contained in:
@@ -422,7 +422,7 @@ abstract class FieldtypeMulti extends Fieldtype {
|
||||
if((int) $query->data('_limit') > 0) {
|
||||
// accommodate paginated value by collecting and passing in pagination details from $query
|
||||
// determine total number of results
|
||||
$query->select('COUNT(*) as _total');
|
||||
$query->set('select', array('COUNT(*) as _total'));
|
||||
$query->set('limit', array()); // clear
|
||||
$query->set('orderby', array()); // clear
|
||||
$stmt = $query->prepare();
|
||||
@@ -476,6 +476,7 @@ abstract class FieldtypeMulti extends Fieldtype {
|
||||
$query->data('_schema', $schema);
|
||||
$query->data('_field', $field);
|
||||
$query->data('_table', $table);
|
||||
$query->data('_filters', $filters);
|
||||
|
||||
foreach($filters as $selector) {
|
||||
|
||||
@@ -520,7 +521,7 @@ abstract class FieldtypeMulti extends Fieldtype {
|
||||
if(empty($orderByCols)) {
|
||||
// if there are no orderByCols defined, pagination & sorting not supported
|
||||
// default sort for FieldtypeMulti fields is by column 'sort'
|
||||
$query->orderby('sort');
|
||||
$query->orderby("$table.sort");
|
||||
|
||||
} else {
|
||||
// one or more orderByCols is defined, enabling sorting and potential pagination
|
||||
|
@@ -9,7 +9,7 @@
|
||||
* /wire/core/Fieldtype.php
|
||||
* /wire/core/FieldtypeMulti.php
|
||||
*
|
||||
* ProcessWire 3.x, Copyright 2018 by Ryan Cramer
|
||||
* ProcessWire 3.x, Copyright 2021 by Ryan Cramer
|
||||
* https://processwire.com
|
||||
*
|
||||
*/
|
||||
@@ -19,10 +19,10 @@ class FieldtypePage extends FieldtypeMulti implements Module, ConfigurableModule
|
||||
public static function getModuleInfo() {
|
||||
return array(
|
||||
'title' => 'Page Reference',
|
||||
'version' => 105,
|
||||
'version' => 106,
|
||||
'summary' => 'Field that stores one or more references to ProcessWire pages',
|
||||
'permanent' => true,
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
const derefAsPageArray = 0;
|
||||
@@ -105,6 +105,18 @@ class FieldtypePage extends FieldtypeMulti implements Module, ConfigurableModule
|
||||
public function isAutoload() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get class name to use Field objects of this type (must be class that extends Field class)
|
||||
*
|
||||
* @param array $a Field data from DB (if needed)
|
||||
* @return string Return class name or blank to use default Field class
|
||||
*
|
||||
*/
|
||||
public function getFieldClass(array $a = array()) {
|
||||
if($a) {} // ignore
|
||||
return 'PageField';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an InputfieldPage of the type configured
|
||||
@@ -120,6 +132,24 @@ class FieldtypePage extends FieldtypeMulti implements Module, ConfigurableModule
|
||||
return $inputfield;
|
||||
}
|
||||
|
||||
/**
|
||||
* Per the Fieldtype interface, Save the given Field from the given Page to the database
|
||||
*
|
||||
* @param Page $page
|
||||
* @param Field $field
|
||||
* @return bool
|
||||
* @throws \PDOException|WireException|WireDatabaseQueryException on failure
|
||||
*
|
||||
*/
|
||||
public function ___savePageField(Page $page, Field $field) {
|
||||
$value = $page->get($field->name);
|
||||
if($value instanceof PageArray) {
|
||||
if($value->data('filters')) throw new WireException('Filtered value is not saveable');
|
||||
if($value->getLimit()) throw new WireException('Paginated value is not saveable');
|
||||
}
|
||||
return parent::___savePageField($page, $field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a raw value (value as stored in DB), return the value as it would appear in a Page object
|
||||
*
|
||||
@@ -134,91 +164,148 @@ class FieldtypePage extends FieldtypeMulti implements Module, ConfigurableModule
|
||||
if($field->hasContext($page)) $field = $field->getContext($page);
|
||||
|
||||
$template = null;
|
||||
$template_ids = self::getTemplateIDs($field);
|
||||
$derefAsPage = $field->get('derefAsPage');
|
||||
$templateIds = self::getTemplateIDs($field);
|
||||
$allowUnpub = $field->get('allowUnpub');
|
||||
|
||||
if(count($template_ids) == 1) {
|
||||
// we only use $template optimization if only one template selected
|
||||
$template = $this->wire('templates')->get(reset($template_ids));
|
||||
}
|
||||
|
||||
// handle $value if it's blank, Page, or PageArray
|
||||
if($derefAsPage > 0) {
|
||||
// value will ultimately be a single Page
|
||||
if(!$value) return $this->getBlankValue($page, $field);
|
||||
|
||||
// if it's already a Page, then we're good: return it
|
||||
if($value instanceof Page) return $value;
|
||||
|
||||
// if it's a PageArray and should be a Page, determine what happens next
|
||||
if($value instanceof PageArray) {
|
||||
// if there's a Page in there, return the first one
|
||||
if(count($value) > 0) return $value->first();
|
||||
|
||||
// it's an empty array, so return whatever our default is
|
||||
return $this->getBlankValue($page, $field);
|
||||
$pagination = array('limit' => null, 'start' => null, 'total' => null);
|
||||
$filters = null; /** @var Selectors|null $filters */
|
||||
|
||||
if(is_array($value)) {
|
||||
// see if pagination and filter data are populated from FieldtypeMulti
|
||||
if(isset($value['_pagination_limit'])) {
|
||||
foreach(array_keys($pagination) as $key) {
|
||||
$valueKey = "_pagination_$key";
|
||||
$pagination[$key] = isset($value[$valueKey]) ? $value[$valueKey] : 0;
|
||||
unset($value[$valueKey]);
|
||||
}
|
||||
}
|
||||
if(isset($value['_filters'])) {
|
||||
$filters = count($value['_filters']) ? $value['_filters'] : null;
|
||||
unset($value['_filters']);
|
||||
}
|
||||
|
||||
} else {
|
||||
// value will ultimately be multiple pages
|
||||
|
||||
// if it's already a PageArray, great, just return it
|
||||
if($value instanceof PageArray) return $value;
|
||||
|
||||
// setup our default/blank value
|
||||
$pageArray = $this->getBlankValue($page, $field);
|
||||
|
||||
// if $value is blank, then return our default/blank value
|
||||
if(empty($value)) return $pageArray;
|
||||
}
|
||||
|
||||
// if we made it this far, then we know that the value was not empty
|
||||
// so it's going to need to be populated from one type to the target type
|
||||
|
||||
// we're going to be dealing with $value as an array this point forward
|
||||
// this is for compatibility with the Pages::getById function
|
||||
if(!is_array($value)) $value = array($value);
|
||||
// $value = $this->validatePageIDs($page, $value);
|
||||
|
||||
if(isset($value['_filters'])) {
|
||||
$filters = $value['_filters'];
|
||||
unset($value['_filters']);
|
||||
if(!count($filters)) $filters = null;
|
||||
if(count($templateIds) == 1) {
|
||||
// we only use $template optimization if only one template selected
|
||||
$template = $this->wire()->templates->get(reset($templateIds));
|
||||
}
|
||||
|
||||
if($field->get('derefAsPage') > 0) {
|
||||
// configured to return single Page, NullPage or false
|
||||
return $this->wakeupValueSingle($page, $field, $template, $value);
|
||||
}
|
||||
|
||||
if($value instanceof PageArray) {
|
||||
$pageArray = $value;
|
||||
} else {
|
||||
$filters = null;
|
||||
if(!is_array($value)) $value = $this->wakeupValueToArray($value);
|
||||
if(count($value)) {
|
||||
$pageArray = $this->wire()->pages->getById($value, $template);
|
||||
} else {
|
||||
$pageArray = $this->getBlankValue($page, $field);
|
||||
}
|
||||
}
|
||||
|
||||
if($derefAsPage > 0) {
|
||||
// we're going to return a single page, NullPage or false
|
||||
$pg = false;
|
||||
if(!$allowUnpub) {
|
||||
// remove any pages that have an unpublished status
|
||||
foreach($pageArray as $p) {
|
||||
// note: these removals can affect pagination accuracy
|
||||
if($p->status >= Page::statusUnpublished) $pageArray->remove($p);
|
||||
}
|
||||
}
|
||||
|
||||
if($pagination['limit'] !== null) {
|
||||
$pageArray->setLimit($pagination['limit']);
|
||||
$pageArray->setStart($pagination['start']);
|
||||
$pageArray->setTotal($pagination['total']);
|
||||
}
|
||||
|
||||
if($filters !== null) $pageArray->data('filters', $filters);
|
||||
|
||||
$pageArray->resetTrackChanges();
|
||||
|
||||
return $pageArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wakeup given string or int value to an array
|
||||
*
|
||||
* @param string|int $value
|
||||
* @return array
|
||||
* @since 3.0.173
|
||||
*
|
||||
*/
|
||||
protected function wakeupValueToArray($value) {
|
||||
if(strpos($value, '|') !== false) {
|
||||
$value = explode('|', $value);
|
||||
} else if(strpos($value, ',') !== false) {
|
||||
$value = explode(',', $value);
|
||||
} else if(ctype_digit($value)) {
|
||||
$value = ((int) $value) > 0 ? array((int) $value) : array();
|
||||
} else {
|
||||
$value = array();
|
||||
}
|
||||
foreach($value as $k => $v) {
|
||||
if(ctype_digit("$v")) {
|
||||
$value[$k] = (int) $v;
|
||||
} else {
|
||||
unset($value[$k]);
|
||||
}
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wakeup single page field value (when derefAsPage>0)
|
||||
*
|
||||
* @param Page $page
|
||||
* @param Field $field
|
||||
* @param Template|null $template
|
||||
* @param array|int|PageArray|Page|null|bool $value
|
||||
* @return bool|Page|PageArray
|
||||
* @since 3.0.173
|
||||
*
|
||||
*/
|
||||
protected function wakeupValueSingle(Page $page, Field $field, $template, $value) {
|
||||
|
||||
$pageValue = false;
|
||||
|
||||
// value will ultimately be a single Page
|
||||
if(empty($value)) {
|
||||
// empty value
|
||||
|
||||
} else if($value instanceof Page) {
|
||||
// already a Page
|
||||
$pageValue = $value;
|
||||
|
||||
} else if($value instanceof PageArray) {
|
||||
// it's a PageArray and should be a Page
|
||||
if($value->count()) {
|
||||
// if there's a Page in there, get just the first one
|
||||
$pageValue = $value->first();
|
||||
}
|
||||
|
||||
} else {
|
||||
// array or other
|
||||
if(!is_array($value)) {
|
||||
$value = ctype_digit("$value") ? array((int) $value) : array();
|
||||
}
|
||||
|
||||
if(count($value)) {
|
||||
// get the first value in a PageArray, using $template and parent for optimization
|
||||
$pageArray = $this->wire('pages')->getById(array((int) reset($value)), $template);
|
||||
if(count($pageArray)) $pg = $pageArray->first();
|
||||
$pageArray = $this->wire()->pages->getById(array((int) reset($value)), $template);
|
||||
if($pageArray->count()) $pageValue = $pageArray->first();
|
||||
}
|
||||
|
||||
if($pg && $pg->status >= Page::statusUnpublished && !$allowUnpub) $pg = false;
|
||||
if(!$pg) $pg = $this->getBlankValue($page, $field);
|
||||
|
||||
return $pg;
|
||||
|
||||
} else {
|
||||
// we're going to return a PageArray
|
||||
if(!count($value)) return $this->getBlankValue($page, $field);
|
||||
if($filters) {
|
||||
$filters->add(new SelectorEqual('id', $value));
|
||||
$finder = $this->pages->getPageFinder();
|
||||
$value = $finder->findIDs($filters);
|
||||
}
|
||||
$pageArray = $this->wire('pages')->getById($value, $template);
|
||||
foreach($pageArray as $pg) {
|
||||
// remove any pages that have an unpublished status
|
||||
if($pg->status >= Page::statusUnpublished && !$allowUnpub) $pageArray->remove($pg);
|
||||
}
|
||||
$pageArray->resetTrackChanges();
|
||||
return $pageArray;
|
||||
}
|
||||
|
||||
if($pageValue && $pageValue->status >= Page::statusUnpublished && !$field->get('allowUnpub')) {
|
||||
$pageValue = false;
|
||||
}
|
||||
|
||||
if(!$pageValue || !$pageValue->id) {
|
||||
$pageValue = $this->getBlankValue($page, $field);
|
||||
}
|
||||
|
||||
return $pageValue;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -724,6 +811,92 @@ class FieldtypePage extends FieldtypeMulti implements Module, ConfigurableModule
|
||||
|
||||
return $pageArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the query used for loading all parts of the data from this field.
|
||||
*
|
||||
* #pw-group-loading
|
||||
*
|
||||
* @param Field|PageField $field
|
||||
* @param DatabaseQuerySelect $query
|
||||
* @return DatabaseQuerySelect
|
||||
* @throws WireException
|
||||
*
|
||||
*/
|
||||
public function getLoadQuery(Field $field, DatabaseQuerySelect $query) {
|
||||
|
||||
$query = parent::getLoadQuery($field, $query);
|
||||
$filters = $query->data('_filters'); /** @var Selectors $filters */
|
||||
|
||||
// this method implementation only applies if filters are in use
|
||||
if(!$filters || !count($filters)) return $query;
|
||||
|
||||
$hasInclude = false;
|
||||
$hasTemplate = false;
|
||||
$hasParent = false;
|
||||
$hasLimit = false;
|
||||
|
||||
foreach($filters as $selector) {
|
||||
$f = $selector->field();
|
||||
if($f === 'limit') {
|
||||
$hasLimit = $selector->value();
|
||||
$filters->remove($selector);
|
||||
} else if($f === 'start') {
|
||||
$filters->remove($selector);
|
||||
} else if($f === 'include') {
|
||||
$hasInclude = $selector->value();
|
||||
} else if($f === 'template' || $f === 'templates_id') {
|
||||
$hasTemplate = $selector->value();
|
||||
} else if($f === 'parent' || $f === 'parent_id') {
|
||||
$hasParent = $selector->value();
|
||||
}
|
||||
}
|
||||
|
||||
if($hasLimit && !$field->allowUnpub) {
|
||||
$fieldTable = $field->getTable();
|
||||
$pagesTable = 'pages_' . $fieldTable;
|
||||
$unpublished = Page::statusUnpublished;
|
||||
$query->join("pages AS $pagesTable ON $pagesTable.id=$fieldTable.data AND $pagesTable.status<$unpublished");
|
||||
// $query->where("$pagesTable.status<$unpublished");
|
||||
}
|
||||
|
||||
if(!count($filters)) {
|
||||
// if removal of start/limit filters resulted in no other filters, exit now
|
||||
return $query;
|
||||
}
|
||||
|
||||
if((!$hasTemplate || !$hasParent) && $field instanceof PageField) {
|
||||
// use configured template and parent data to better narrow in on filtered results
|
||||
$data = $field->getTemplateAndParentIds();
|
||||
if(!$hasTemplate) {
|
||||
$templateIds = $data['templateIds'];
|
||||
if(count($templateIds)) $filters->add(new SelectorEqual('templates_id', $templateIds));
|
||||
}
|
||||
if(!$hasParent) {
|
||||
$parentIds = $data['parentIds'];
|
||||
if(count($parentIds)) $filters->add(new SelectorEqual('parent_id', $parentIds));
|
||||
}
|
||||
}
|
||||
|
||||
if(!$hasInclude) {
|
||||
if($field->get('allowUnpub')) {
|
||||
$filters->add(new SelectorEqual('include', 'unpublished'));
|
||||
} else {
|
||||
$filters->add(new SelectorEqual('include', 'hidden'));
|
||||
}
|
||||
}
|
||||
|
||||
$pageIds = $this->wire()->pages->findIDs($filters);
|
||||
|
||||
if(count($pageIds)) {
|
||||
$table = $this->wire()->database->escapeTable($field->table);
|
||||
$query->where("$table.data IN(" . implode(',', $pageIds) . ")");
|
||||
} else {
|
||||
$query->where('1>2');
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a where condition to a load query (used by getLoadQuery method)
|
||||
@@ -737,7 +910,7 @@ class FieldtypePage extends FieldtypeMulti implements Module, ConfigurableModule
|
||||
*
|
||||
*/
|
||||
protected function getLoadQueryWhere(Field $field, DatabaseQuerySelect $query, $col, $operator, $value) {
|
||||
// cancel the default behavior since Page fields filter from the wakeupValue method instead
|
||||
// cancel the default behavior since Page fields filter from getLoadQuery() instead
|
||||
return $query;
|
||||
}
|
||||
|
||||
@@ -1583,3 +1756,4 @@ class FieldtypePage extends FieldtypeMulti implements Module, ConfigurableModule
|
||||
|
||||
}
|
||||
|
||||
require_once(__DIR__ . '/PageField.php');
|
||||
|
124
wire/modules/Fieldtype/PageField.php
Normal file
124
wire/modules/Fieldtype/PageField.php
Normal file
@@ -0,0 +1,124 @@
|
||||
<?php namespace ProcessWire;
|
||||
|
||||
/**
|
||||
* Page Field (for FieldtypePage)
|
||||
*
|
||||
* Configured with FieldtypePage
|
||||
* ==============================
|
||||
* @property int $derefAsPage
|
||||
* @property int|bool $allowUnpub
|
||||
*
|
||||
* Configured with InputfieldPage
|
||||
* ==============================
|
||||
* @property int $template_id
|
||||
* @property array $template_ids
|
||||
* @property int $parent_id
|
||||
* @property string $inputfield Inputfield class used for input
|
||||
* @property string $labelFieldName Field name to use for label (note: this will be "." if $labelFieldFormat is in use).
|
||||
* @property string $labelFieldFormat Formatting string for $page->getMarkup() as alternative to $labelFieldName
|
||||
* @property string $findPagesCode
|
||||
* @property string $findPagesSelector
|
||||
* @property string $findPagesSelect Same as findPageSelector, but configured interactively with InputfieldSelector.
|
||||
* @property int|bool $addable
|
||||
* @property-read string $inputfieldClass Public property alias of protected getInputfieldClass() method
|
||||
* @property array $inputfieldClasses
|
||||
*
|
||||
* @since 3.0.173
|
||||
*
|
||||
*/
|
||||
class PageField extends Field {
|
||||
|
||||
/**
|
||||
* Return array configured template and parent IDs identified in field configuration
|
||||
*
|
||||
* #pw-internal
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
*/
|
||||
public function getTemplateAndParentIds() {
|
||||
|
||||
$parentId = $this->get('parent_id');
|
||||
$parentIds = array();
|
||||
$templateIds = array();
|
||||
|
||||
if(empty($parentId)) {
|
||||
$parentIds = array();
|
||||
} else if(is_string($parentId)) {
|
||||
if(ctype_digit($parentId)) {
|
||||
$parentIds = array((int) $parentId);
|
||||
} else if(strpos($parentId, '|') !== false) {
|
||||
$parentIds = explode('|', $parentId);
|
||||
}
|
||||
} else if(is_int($parentId)) {
|
||||
$parentIds = array($parentId);
|
||||
} else if(is_array($parentId)) {
|
||||
$parentIds = array_values($parentId);
|
||||
}
|
||||
|
||||
foreach(array('template_id', 'template_ids') as $key) {
|
||||
$value = $this->get($key);
|
||||
if(empty($value)) continue;
|
||||
if(!is_array($value)) $value = array($value);
|
||||
foreach($value as $id) {
|
||||
$id = (int) $id;
|
||||
if($id > 0) $templateIds[$id] = $id;
|
||||
}
|
||||
}
|
||||
|
||||
foreach(array('findPagesSelect', 'findPagesSelector') as $key) {
|
||||
|
||||
$selector = $this->get($key);
|
||||
if(empty($selector)) continue;
|
||||
if(strpos($selector, 'parent') === false || strpos($selector, 'template') === false) continue;
|
||||
|
||||
foreach(new Selectors($selector) as $s) {
|
||||
if(!$s instanceof SelectorEqual) continue;
|
||||
|
||||
if($s->field() === 'parent') {
|
||||
foreach($s->values() as $v) {
|
||||
if(ctype_digit("$v")) {
|
||||
$parentIds[] = (int) $v;
|
||||
} else if(strpos($v, '/')) {
|
||||
$p = $this->wire()->pages->get($v);
|
||||
if($p->id) $parentIds[] = $p->id;
|
||||
}
|
||||
}
|
||||
|
||||
} else if($s->field() === 'parent_id') {
|
||||
$parentIds = array_merge($parentIds, $s->values());
|
||||
|
||||
} else if($s->field() === 'template' || $s->field() === 'templates_id') {
|
||||
foreach($s->values() as $v) {
|
||||
if(ctype_digit("$v")) {
|
||||
$templateIds[] = (int) $v;
|
||||
} else if($v) {
|
||||
$template = $this->wire()->templates->get($v);
|
||||
if($template instanceof Template) $templateIds[] = $template->id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(count($parentIds)) {
|
||||
foreach($parentIds as $key => $id) {
|
||||
$parentIds[$key] = (int) $id;
|
||||
}
|
||||
$parentIds = array_unique($parentIds);
|
||||
}
|
||||
|
||||
if(count($templateIds)) {
|
||||
foreach($templateIds as $key => $id) {
|
||||
$templateIds[$key] = (int) $id;
|
||||
}
|
||||
$templateIds = array_unique($templateIds);
|
||||
}
|
||||
|
||||
return array(
|
||||
'parentIds' => $parentIds,
|
||||
'templateIds' => $templateIds,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user