1
0
mirror of https://github.com/processwire/processwire.git synced 2025-08-14 02:34:24 +02:00

Add support for custom Page classes

This commit is contained in:
Ryan Cramer
2020-03-06 14:13:22 -05:00
parent 15793931f4
commit 273183ddfb
19 changed files with 254 additions and 91 deletions

View File

@@ -143,6 +143,7 @@
* @property bool $usePoweredBy Use the x-powered-by header? Set to false to disable. #pw-group-system
* @property bool $useFunctionsAPI Allow most API variables to be accessed as functions? (see /wire/core/FunctionsAPI.php) #pw-group-system
* @property bool $useMarkupRegions Enable support for front-end markup regions? #pw-group-system
* @property bool $usePageClasses Use custom Page classes in `/site/classes/[TemplateName]Page.php`? #pw-group-system #since 3.0.152
* @property int $lazyPageChunkSize Chunk size for for $pages->findMany() calls. #pw-group-system
*
* @property string $userAuthSalt Salt generated at install time to be used as a secondary/non-database salt for the password system. #pw-group-session

View File

@@ -1239,30 +1239,43 @@ class Pages extends Wire {
*
* #pw-internal
*
* @param array $options Optionally specify array of any of the following:
* - `pageClass` (string): Class to use for Page object (default='Page').
* - `template` (Template|id|string): Template to use.
* - Plus any other Page properties or fields you want to set at this time
* @param array|string|Template $options Optionally specify array of any of the following:
* - `template` (Template|id|string): Template to use via object, ID or name.
* - `pageClass` (string): Class to use for Page. If not specified, default is from template setting, or 'Page' if no template.
* - Any other Page properties or fields you want to set (parent, name, title, etc.). Note that most page fields will need to
* have a `template` set first, so make sure to include it in your options array when providing other fields.
* - In PW 3.0.152+ you may specify the Template object, name or ID instead of an $options array.
* @return Page
*
*/
public function newPage(array $options = array()) {
$class = 'Page';
if(!empty($options['pageClass'])) $class = $options['pageClass'];
if(isset($options['template'])) {
public function newPage($options = array()) {
if(!is_array($options)) {
if(is_object($options) && $options instanceof Template) {
$options = array('template' => $options);
} else if($options && (is_string($options) || is_int($options))) {
$options = array('template' => $options);
} else {
$options = array();
}
}
if(!empty($options['pageClass'])) {
$class = $options['pageClass'];
} else {
$class = 'Page';
}
if(!empty($options['template'])) {
$template = $options['template'];
if(!is_object($template)) {
$template = empty($template) ? null : $this->wire('templates')->get($template);
}
if($template && empty($options['pageClass']) && $template->pageClass) {
$class = $template->pageClass;
if(!wireClassExists($class)) $class = 'Page';
}
if($template && empty($options['pageClass'])) {
$class = $template->getPageClass();
}
} else {
$template = null;
}
$class = wireClassName($class, true);
if(strpos($class, "\\") === false) $class = wireClassName($class, true);
$page = $this->wire(new $class($template));
if(!$page instanceof Page) $page = $this->wire(new Page($template));

View File

@@ -75,12 +75,7 @@ class PagesEditor extends Wire {
if(!$template) throw new WireException("Unknown template");
}
$pageClass = wireClassName($template->pageClass ? $template->pageClass : 'Page', true);
$page = $this->pages->newPage(array(
'template' => $template,
'pageClass' => $pageClass
));
$page = $this->pages->newPage($template);
$page->parent = $parent;
$exceptionMessage = "Unable to add new page using template '$template' and parent '{$page->parent->path}'.";

View File

@@ -267,10 +267,15 @@ class PagesLoader extends Wire {
$profilerEvent = $profiler ? $profiler->start("$caller($selectorString)", "Pages") : null;
if(($lazy || $findIDs) && strpos($selectorString, 'limit=') === false) $options['getTotal'] = false;
if($lazy || $findIDs === 1) {
$pagesIDs = $pageFinder->findIDs($selectors, $options);
if($lazy) {
// [ pageID => templateID ]
$pagesIDs = $pageFinder->findTemplateIDs($selectors, $options);
} else if($findIDs === 1) {
// [ pageID ]
$pagesIDs = $pageFinder->findIDs($selectors, $options);
} else {
// [ [ 'id' => 3, 'templates_id' => 2, 'parent_id' => 1 ]
$pagesInfo = $pageFinder->find($selectors, $options);
}
@@ -292,9 +297,16 @@ class PagesLoader extends Wire {
$loadPages = false;
$cachePages = false;
$template = null;
$templatesByID = array();
foreach($pagesIDs as $id) {
$page = $this->pages->newPage();
foreach($pagesIDs as $id => $templateID) {
if(isset($templatesByID[$templateID])) {
$template = $templatesByID[$templateID];
} else {
$template = $this->wire('templates')->get($templateID);
$templatesByID[$templateID] = $template;
}
$page = $this->pages->newPage($template);
$page->_lazy($id);
$page->loaderCache = false;
$pages->add($page);
@@ -302,6 +314,7 @@ class PagesLoader extends Wire {
$pages->setDuplicateChecking(true);
if(count($pagesIDs)) $pages->_lazy(true);
unset($template, $templatesByID);
} else if($findIDs) {
@@ -553,6 +566,9 @@ class PagesLoader extends Wire {
'pageArrayClass' => 'PageArray',
'caller' => '',
);
/** @var Templates $templates */
$templates = $this->wire('templates');
if(is_array($template)) {
// $template property specifies an array of options
@@ -570,7 +586,7 @@ class PagesLoader extends Wire {
if(!is_null($template) && !is_object($template)) {
// convert template string or id to Template object
$template = $this->wire('templates')->get($template);
$template = $templates->get($template);
}
if(is_string($_ids)) {
@@ -696,7 +712,7 @@ class PagesLoader extends Wire {
foreach($idsByTemplate as $templates_id => $ids) {
if($templates_id && (!$template || $template->id != $templates_id)) {
$template = $this->wire('templates')->get($templates_id);
$template = $templates->get($templates_id);
}
if($template) {
@@ -748,25 +764,8 @@ class PagesLoader extends Wire {
$database->execute($stmt);
$class = $options['pageClass'];
if(empty($class)) {
if($template) {
$class = ($template->pageClass && wireClassExists($template->pageClass)) ? $template->pageClass : 'Page';
} else {
$class = 'Page';
}
}
if(empty($class)) $class = $template ? $template->getPageClass() : __NAMESPACE__ . "\\Page";
$_class = wireClassName($class, true);
if($class != 'Page' && !wireClassExists($_class)) {
if(class_exists("\\$class")) {
$_class = "\\$class";
} else {
$this->error("Class '$class' for Pages::getById() does not exist", Notice::log);
$class = 'Page';
$_class = wireClassName($class, true);
}
}
// page to populate, if provided in 'getOne' mode
/** @var Page|null $_page */
$_page = $options['getOne'] && $options['page'] && $options['page'] instanceof Page ? $options['page'] : null;
@@ -781,9 +780,11 @@ class PagesLoader extends Wire {
$page->set('template', $template ? $template : $row['templates_id']);
} else {
// create new Page object
$pageTemplate = $template ? $template : $templates->get((int) $row['templates_id']);
$pageClass = empty($options['pageClass']) && $pageTemplate ? $pageTemplate->getPageClass() : $class;
$page = $this->pages->newPage(array(
'pageClass' => $_class,
'template' => $template ? $template : $row['templates_id'],
'pageClass' => $pageClass,
'template' => $pageTemplate ? $pageTemplate : $row['templates_id'],
));
}
unset($row['templates_id']);
@@ -902,7 +903,7 @@ class PagesLoader extends Wire {
if($languageID) $languages->unsetLanguage();
return $path;
} else if($id == $homepageID && $languages && !$languageID) {
} else if($id === $homepageID && $languages && !$languageID) {
// default language in multi-language environment, let $page handle it since there is additional
// hooked logic there provided by LanguageSupportPageNames
$page = $this->pages->get($homepageID);

View File

@@ -274,17 +274,29 @@ class PagesType extends Wire implements \IteratorAggregate, \Countable {
$pageClass = $page->className();
if($this->pageClass && $pageClass === $this->pageClass) return true;
$valid = false;
foreach($this->templates as $template) {
/** @var Template $template */
if($template->pageClass) {
// template specifies a class
if($template->pageClass === $pageClass) $valid = true;
} else {
if($template->pageClass === $pageClass) {
$valid = true;
} else if(wireInstanceOf($page, $template->pageClass)) {
$valid = true;
}
} else if($pageClass === 'Page') {
// template specifies NO Page class, which implies "Page" as a class name is valid
if($pageClass === 'Page') $valid = true;
$valid = true;
} else {
// page has some other class name
$className = $template->getPageClass();
if(wireClassName($className) === $pageClass || wireInstanceOf($page, $className)) {
$valid = true;
}
}
if($valid) break;
}
return $valid;
}
@@ -588,8 +600,13 @@ class PagesType extends Wire implements \IteratorAggregate, \Countable {
*
*/
public function getPageClass() {
if($this->template) {
if($this->pageClass && !$this->template->pageClass) {
$this->template->pageClass = $this->pageClass;
}
return $this->template->getPageClass(false);
}
if($this->pageClass) return $this->pageClass;
if($this->template && $this->template->pageClass) return $this->template->pageClass;
return 'Page';
}

View File

@@ -48,7 +48,7 @@
*
* #pw-body
*
* ProcessWire 3.x, Copyright 2019 by Ryan Cramer
* ProcessWire 3.x, Copyright 2020 by Ryan Cramer
* https://processwire.com
*
* This file is licensed under the MIT license
@@ -68,6 +68,7 @@
* @property string $files Site-specific files: /site/assets/files/
* @property string $tmp Temporary files: /site/assets/tmp/ #pw-group-paths-only
* @property string $sessions Session files: /site/assets/sessions/ #pw-group-paths-only
* @property string $classes Site-specific class files: /site/classes/ #pw-group-paths-only
*
* The following properties are only in $config->urls
* ==================================================

View File

@@ -13,7 +13,7 @@
* Please be sure to see the `Module` interface for full details on methods you can specify in a Process module.
* #pw-body
*
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* ProcessWire 3.x, Copyright 2020 by Ryan Cramer
* https://processwire.com
*
* This file is licensed under the MIT license
@@ -371,8 +371,7 @@ abstract class Process extends WireData implements Module {
if(!$parent || !$parent->id) $parent = $adminPage; // default
$page = $parent->child("include=all, name=$name"); // does it already exist?
if($page->id && "$page->process" == "$this") return $page; // return existing copy
$page = $this->wire('pages')->newPage();
$page->template = $template ? $template : 'admin';
$page = $this->wire('pages')->newPage($template ? $template : 'admin');
$page->name = $name;
$page->parent = $parent;
$page->process = $this;

View File

@@ -258,9 +258,14 @@ class ProcessWire extends Wire {
$this->fuel = new Fuel();
$this->fuel->set('wire', $this, true);
/** @var WireClassLoader $classLoader */
$classLoader = $this->wire('classLoader', new WireClassLoader($this), true);
$classLoader->addNamespace((strlen(__NAMESPACE__) ? __NAMESPACE__ : "\\"), PROCESSWIRE_CORE_PATH);
if($config->usePageClasses) {
$classLoader->addSuffix('Page', $config->paths->classes);
}
$this->wire('hooks', new WireHooks($this, $config), true);
$this->setConfig($config);
@@ -1101,6 +1106,7 @@ class ProcessWire extends Wire {
$cfg['paths'] = clone $cfg['urls'];
$cfg['paths']->set('root', $rootPath . '/');
$cfg['paths']->data('sessions', $cfg['paths']->assets . "sessions/");
$cfg['paths']->data('classes', $cfg['paths']->site . "classes/");
// Styles and scripts are CSS and JS files, as used by the admin application.
// But reserved here if needed by other apps and templates.

View File

@@ -1177,6 +1177,23 @@ class Template extends WireData implements Saveable, Exportable {
$langs->add($languages->getDefault());
return $langs;
}
/**
* Get class name to use for Page objects using this template
*
* Note that value can be different from the `$template->pageClass` property, since it is determined at runtime.
* If it is different, then it is at least a class that extends the one defined by the pageClass property.
*
* #pw-group-identification
*
* @param bool $withNamespace Returned class includes namespace? (default=true)
* @return string Returned page class includes namespace
* @since 3.0.152
*
*/
public function getPageClass($withNamespace = true) {
return $this->wire('templates')->getPageClass($this, $withNamespace);
}
/**
* Set the icon to use with this template

View File

@@ -41,6 +41,14 @@ class Templates extends WireSaveableItems {
*/
protected $fileModTemplates = array();
/**
* Cached template ID to page class names (for getPageClass method)
*
* @var array
*
*/
protected $pageClassNames = array();
/**
* Construct the Templates
*
@@ -532,7 +540,7 @@ class Templates extends WireSaveableItems {
if($checkAccess) {
if($parentPage->id) {
// single defined parent
$p = $this->wire('pages')->newPage(array('template' => $template));
$p = $this->wire('pages')->newPage($template);
if(!$parentPage->addable($p)) continue;
} else {
// multiple possible parents
@@ -546,7 +554,7 @@ class Templates extends WireSaveableItems {
}
if($checkAccess && $getAll && $foundParents && $foundParents->count()) {
$p = $this->wire('pages')->newPage(array('template' => $template));
$p = $this->wire('pages')->newPage($template);
foreach($foundParents as $parentPage) {
if(!$parentPage->addable($p)) $foundParents->remove($parentPage);
}
@@ -572,6 +580,87 @@ class Templates extends WireSaveableItems {
return $this->getParentPage($template, $checkAccess, $getAll);
}
/**
* Get class name to use for pages using given Template
*
* Note that value can be different from the `$template->pageClass` property, since it is determined at runtime.
* If it is different, then it is at least a class that extends the one defined by pageClass.
*
* @param Template $template
* @param bool $withNamespace Include namespace? (default=true)
* @return string Returned class name includes namespace
* @since 3.0.152
*
*/
public function getPageClass(Template $template, $withNamespace = true) {
if(isset($this->pageClassNames[$template->id])) {
// use cached value when present
$pageClass = $this->pageClassNames[$template->id];
if(!$withNamespace) $pageClass = wireClassName($pageClass, false);
return $pageClass;
}
$corePageClass = __NAMESPACE__ . "\\Page";
// first check for class defined with Template 'pageClass' setting
$pageClass = $template->pageClass;
if($pageClass && $pageClass !== 'Page') {
// page has custom class assignment in its template
$nsPageClass = wireClassName($pageClass, true);
// is this custom class available for instantiation?
if(class_exists($nsPageClass)) {
// class is available for use and has a namespace
$pageClass = $nsPageClass;
} else if(class_exists("\\$pageClass") && wireInstanceOf("\\$pageClass", $corePageClass)) {
// class appears to be available in root namespace and it extends PWs Page class (legacy)
$pageClass = "\\$pageClass";
} else {
// class is not available for instantiation
$pageClass = '';
}
}
$config = $this->wire('config');
$usePageClasses = $config->usePageClasses;
if(empty($pageClass) || $pageClass === 'Page') {
// if no custom Page class available, use default Page class with namespace
if($usePageClasses) {
// custom classes enabled
if(!isset($this->pageClassNames[0])) {
// index 0 holds cached default page class
$defaultPageClass = __NAMESPACE__ . "\\DefaultPage";
if(!class_exists($defaultPageClass) || !wireInstanceOf($defaultPageClass, $corePageClass)) {
$defaultPageClass = $corePageClass;
}
$this->pageClassNames[0] = $defaultPageClass;
}
$pageClass = $this->pageClassNames[0];
} else {
$pageClass = $corePageClass;
}
}
// determine if custom class available (3.0.152+)
if($usePageClasses) {
// generate a CamelCase name + 'Page' from template name, i.e. 'blog-post' => 'BlogPostPage'
$className = ucwords(str_replace(array('-', '_', '.'), ' ', $template->name));
$className = __NAMESPACE__ . "\\" . str_replace(' ', '', $className) . 'Page';
if(class_exists($className) && wireInstanceOf($className, $corePageClass)) {
$pageClass = $className;
}
}
if($template->id) $this->pageClassNames[$template->id] = $pageClass;
if(!$withNamespace) $pageClass = wireClassName($pageClass, false);
return $pageClass;
}
/**
* Set a Permission for a Template for and specific Role
*

View File

@@ -134,9 +134,12 @@ class Users extends PagesType {
*
*/
public function newUser() {
$template = $this->wire('templates')->get('user');
$pageClass = $template ? $template->getPageClass(false) : 'User';
if($pageClass !== 'User' && strpos($pageClass, 'User') === false) $pageClass = 'User';
return $this->wire('pages')->newPage(array(
'template' => 'user',
'pageClass' => 'User'
'pageClass' => $pageClass
));
}