diff --git a/wire/core/FieldtypeMulti.php b/wire/core/FieldtypeMulti.php index 108fabc3..abf2b423 100644 --- a/wire/core/FieldtypeMulti.php +++ b/wire/core/FieldtypeMulti.php @@ -452,12 +452,13 @@ abstract class FieldtypeMulti extends Fieldtype { */ public function getLoadQuery(Field $field, DatabaseQuerySelect $query) { - $database = $this->wire('database'); + $database = $this->wire()->database; + $sanitizer = $this->wire()->sanitizer; + $table = $database->escapeTable($field->table); $schemaAll = $this->getDatabaseSchema($field); $schema = $this->trimDatabaseSchema($schemaAll); $fieldName = $database->escapeCol($field->name); - $sanitizer = $this->wire('sanitizer'); $orderByCols = array(); $start = null; $limit = null; diff --git a/wire/core/Page.php b/wire/core/Page.php index fc5c117e..31f898fd 100644 --- a/wire/core/Page.php +++ b/wire/core/Page.php @@ -97,6 +97,7 @@ * * * @property Page|null $_cloning Internal runtime use, contains Page being cloned (source), when this Page is the new copy (target). #pw-internal + * @property int |null $_inserted Populated with time() value of when new page was inserted into DB, only for page created in this request. #pw-internal * @property bool|null $_hasAutogenName Internal runtime use, set by Pages class when page as auto-generated name. #pw-internal * @property bool|null $_forceSaveParents Internal runtime/debugging use, force a page to refresh its pages_parents DB entries on save(). #pw-internal * @property float|null $_pfscore Internal PageFinder fulltext match score when page found/loaded from relevant query. #pw-internal diff --git a/wire/core/Pages.php b/wire/core/Pages.php index e26469c0..b842ccf2 100644 --- a/wire/core/Pages.php +++ b/wire/core/Pages.php @@ -841,7 +841,9 @@ class Pages extends Wire { /** * Add a new page using the given template and parent * - * If no page "name" is specified, one will be automatically assigned. + * If no page “name” is specified, one will be automatically assigned. + * + * For an alternate interface for adding new pages, see the `$pages->new()` method. * * ~~~~~ * // Add new page using 'skyscraper' template into Atlanta @@ -859,7 +861,7 @@ class Pages extends Wire { * ]); * ~~~~~ * - * #pw-group-manipulation + * #pw-group-creation * * @param string|Template $template Template name or Template object * @param string|int|Page $parent Parent path, ID or Page object @@ -868,11 +870,83 @@ class Pages extends Wire { * @param array $values Field values to assign to page (optional). If $name is omitted, this may also be 3rd param. * @return Page New page ready to populate. Note that this page has output formatting off. * @throws WireException When some criteria prevents the page from being saved. + * @see Pages::new(), Pages::newPage() * */ public function ___add($template, $parent, $name = '', array $values = array()) { return $this->editor()->add($template, $parent, $name, $values); } + + /** + * Create a new Page populated from selector string or array + * + * This is similar to the `$pages->add()` method but with a simpler 1-argument (selector) interface. + * This method can also auto-detect some properties that the add() method cannot. + * + * To create a new page without saving to the database use the `$pages->newPage()` method instead. + * It accepts the same arguments as this method. + * + * Minimum requirements to create a new page that is saved in database: + * + * - A `template` must be specified, unless it can be auto-detected from a given `parent`. + * - A `parent` must be specified, unless it can be auto-detected from a given `template` or `path`. + * + * Please note the following: + * + * - If a `path` is specified but not a `name` or `parent` then both will be derived from the `path`. + * - If a `title` is specified but not a `name` or `path` then the `name` will be derived from the `title`. + * - If given `parent` or `path` only allows one template (via family settings) then `template` becomes optional. + * - If given `template` only allows one parent (via family settings) then `parent` becomes optional. + * - If given selector string starts with a `/` it is assumed to be the `path` property. + * - If new page has name that collides with an existing page (i.e. “foo”), new page name will increment (i.e. “foo-1”). + * - If no `name`, `path` or `title` is given (that name can be derived from) then an “untitled-page” name will be used. + * + * ~~~~~ + * // Creating a page via selector string + * $p = $pages->new("template=category, parent=/categories/, title=New Category"); + * + * // Creating a page via selector using path, which implies parent and name + * $p = $pages->new("template=category, path=/categories/new-category"); + * + * // Creating a page via array + * $p = $pages->new([ + * 'template' => 'category', + * 'parent' => '/categories/', + * 'title' => 'New Category' + * ]); + * + * // Parent and name can be auto-detected when you specify path… + * $p = $pages->new('path=/blog/posts/foo-bar-baz'); + * + * // …and even 'path=' is optional if slash '/' is at beginning + * $p = $pages->new('/blog/posts/foo-bar-baz'); + * ~~~~~ + * + * #pw-group-creation + * + * @param string|array $selector Selector string or array of properties to set + * @return Page + * @since 3.0.191 + * @see Pages::add(), Pages::newPage() + * + */ + public function ___new($selector = '') { + + $options = $this->editor()->newPageOptions($selector); + $error = 'Cannot save new page without a %s'; + $template = isset($options['template']) ? $options['template'] : null; + $parent = isset($options['parent']) ? $options['parent'] : null; + $name = isset($options['name']) ? $options['name'] : ''; + + if(!$template) throw new WireException(sprintf($error, 'template')); + if(!$parent) throw new WireException(sprintf($error, 'parent')); + + unset($options['template'], $options['parent'], $options['name']); + + $page = $this->editor()->add($template, $parent, $name, $options); + + return $page; + } /** * Clone entire page return it. @@ -897,7 +971,7 @@ class Pages extends Wire { * $copy->save(); * ~~~~~ * - * #pw-group-manipulation + * #pw-group-creation * * @param Page $page Page that you want to clone * @param Page|null $parent New parent, if different (default=null, which implies same parent) @@ -1714,6 +1788,7 @@ class Pages extends Wire { * * @param array $options Optionally specify ONE of the following: * - `pageArrayClass` (string): Name of PageArray class to use (if not “PageArray”). + * - `class` (string): Alias of pageArrayClass supported in 3.0.191+. * - `pageArray` (PageArray): Wire and return this given PageArray, rather than instantiating a new one. * @return PageArray * @@ -1724,7 +1799,11 @@ class Pages extends Wire { return $options['pageArray']; } $class = 'PageArray'; - if(!empty($options['pageArrayClass'])) $class = $options['pageArrayClass']; + if(!empty($options['pageArrayClass'])) { + $class = $options['pageArrayClass']; + } else if(!empty($options['class'])) { + $class = $options['class']; + } $class = wireClassName($class, true); $pageArray = $this->wire(new $class()); if(!$pageArray instanceof PageArray) $pageArray = $this->wire(new PageArray()); @@ -1732,54 +1811,70 @@ class Pages extends Wire { } /** - * Return a new/blank Page object (in memory only) + * Return a new Page object without saving it to the database * - * #pw-internal + * To create a new Page object and save it the database, use the `$pages->new()` or `$pages->add()` methods, + * or call `save()` on the Page object returned from this method. * - * @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 + * - When a template is specified, the `pageClass` can be auto-detected from the template. + * - In 3.0.152+ you may specify the Template object, name or ID instead of an $options array. + * - In 3.0.191+ you may specify a selector string for the $options argument (alternative to array), + * see the `$pages->new()` method `$selector` argument for details. + * - In 3.0.191+ the `pageClass` can also be specified as `class`, assuming that doesn’t collide + * with an existing field name. + * + * ~~~~~ + * // Create a new blank Page object + * $p = $pages->newPage(); + * + * // Create a new Page object and specify properties to set with an array + * $p = $pages->newPage([ + * 'template' => 'blog-post', + * 'parent' => '/blog/posts/', + * 'title' => 'Hello world', + * ]); + * + * // Same as above but using selector string (3.0.191+) + * $p = $pages->newPage('template=blog-post, parent=/blog/posts, title=Hello world'); + * + * // Create new Page object using 'blog-post' template + * $p = $pages->newPage('blog-post'); + * + * // Create new Page object with parent and name implied by given path (3.0.191+) + * $p = $pages->newPage('/blog/posts/hello-world'); + * ~~~~~ + * + * #pw-group-creation + * + * @param array|string|Template $options Optionally specify array (or selector string in 3.0.191+) with any of the following: + * - `template` (Template|id|string): Template to use via object, ID or name. The `pageClass` will be auto-detected. + * - `parent` (Page|int|string): Parent page object, ID or path. + * - `name` (string): Name of page. + * - `path` (string): Specify /path/for/page/, name and parent (and maybe template) can be auto-detected. 3.0.191+ + * - `pageClass` (string): Class to use for Page. If not specified, default is from template setting, or `Page` if no template. + * - Specify any other Page properties or fields you want to set (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($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'])) { - $class = $template->getPageClass(); - } - } else { - $template = null; - } + if(empty($options)) return $this->wire(new Page()); + + $options = $this->editor()->newPageOptions($options); + $template = isset($options['template']) ? $options['template'] : null; + $parent = isset($options['parent']) ? $options['parent'] : null; + $class = empty($options['pageClass']) ? 'Page' : $options['pageClass']; + + unset($options['template'], $options['parent'], $options['pageClass']); + if(strpos($class, "\\") === false) $class = wireClassName($class, true); - $page = $this->wire(new $class($template)); - if(!$page instanceof Page) $page = $this->wire(new Page($template)); - unset($options['pageClass'], $options['template']); - foreach($options as $name => $value) { - $page->set($name, $value); - } + $page = $this->wire(new $class($template)); + + if(!$page instanceof Page) $page = $this->wire(new Page($template)); + if($parent && $parent->id) $page->parent = $parent; + if(count($options)) $page->setArray($options); return $page; } @@ -2280,8 +2375,10 @@ class Pages extends Wire { $status = $page->status; $statusPrevious = $page->statusPrevious; $isPublished = !$page->isUnpublished(); + $isInserted = $page->_inserted > 0; $wasPublished = !($statusPrevious & Page::statusUnpublished); - if($isPublished && !$wasPublished) $this->published($page); + + if($isPublished && (!$wasPublished || $isInserted)) $this->published($page); if(!$isPublished && $wasPublished) $this->unpublished($page); $from = array(); diff --git a/wire/core/PagesEditor.php b/wire/core/PagesEditor.php index 9c223423..a2778084 100644 --- a/wire/core/PagesEditor.php +++ b/wire/core/PagesEditor.php @@ -81,8 +81,12 @@ class PagesEditor extends Wire { if(!$template) throw new WireException("Unknown template"); } - $page = $this->pages->newPage($template); - $page->parent = $parent; + $options = array('template' => $template, 'parent' => $parent); + if(isset($values['pageClass'])) { + $options['pageClass'] = $values['pageClass']; + unset($values['pageClass']); + } + $page = $this->pages->newPage($options); $exceptionMessage = "Unable to add new page using template '$template' and parent '{$page->parent->path}'."; @@ -120,11 +124,13 @@ class PagesEditor extends Wire { // get a fresh copy of the page if($page->id) { + $inserted = $page->_inserted; $of = $this->pages->outputFormatting; if($of) $this->pages->setOutputFormatting(false); $p = $this->pages->getById($page->id, $template, $page->parent_id); if($p->id) $page = $p; if($of) $this->pages->setOutputFormatting(true); + $page->setQuietly('_inserted', $inserted); } return $page; @@ -596,7 +602,11 @@ class PagesEditor extends Wire { } } while($keepTrying && (++$tries < $maxTries)); - if($result && ($isNew || !$page->id)) $page->id = (int) $database->lastInsertId(); + if($result && ($isNew || !$page->id)) { + $page->id = (int) $database->lastInsertId(); + $page->setQuietly('_inserted', time()); + } + if($options['forceID']) $page->id = (int) $options['forceID']; return $result; @@ -1724,6 +1734,149 @@ class PagesEditor extends Wire { return true; } + + /** + * Prepare options for Pages::new(), Pages::newPage() + * + * Converts given array, selector string, template name, object or int to array of options. + * + * #pw-internal + * + * @param array|string|int $options + * @return array + * @since 3.0.191 + * + */ + public function newPageOptions($options) { + + if(empty($options)) return array(); + + $template = null; /** @var Template|null $template */ + $parent = null; + $class = ''; + + if(is_array($options)) { + // ok + } else if(is_string($options)) { + if(strpos($options, '=') !== false) { + $selectors = new Selectors($options); + $this->wire($selectors); + $options = array(); + foreach($selectors as $selector) { + $options[$selector->field()] = $selector->value; + } + } else if(strpos($options, '/') === 0) { + $options = array('path' => $options); + } else { + $options = array('template' => $options); + } + } else if(is_object($options)) { + $options = $options instanceof Template ? array('template' => $options) : array(); + } else if(is_int($options)) { + $template = $this->wire()->templates->get($options); + $options = $template ? array('template' => $template) : array(); + } else { + $options = array(); + } + + // only use property 'parent' rather than 'parent_id' + if(!empty($options['parent_id']) && empty($options['parent'])) { + $options['parent'] = $options['parent_id']; + unset($options['parent_id']); + } + + // only use property 'template' rather than 'templates_id' + if(!empty($options['templates_id']) && empty($options['template'])) { + $options['template'] = $options['templates_id']; + unset($options['templates_id']); + } + + // page class (pageClass) + if(!empty($options['pageClass'])) { + // ok + $class = $options['pageClass']; + unset($options['pageClass']); + } else if(!empty($options['class']) && !$this->wire()->fields->get('class')) { + // alias for pageClass, so long as there is not a field named 'class' + $class = $options['class']; + unset($options['class']); + } + + // identify requested template + if(isset($options['template'])) { + $template = $options['template']; + if(!is_object($template)) { + $template = empty($template) ? null : $this->wire()->templates->get($template); + } + unset($options['template']); + } + + // convert parent path to parent page object + if(!empty($options['parent'])) { + if(is_object($options['parent'])) { + $parent = $options['parent']; + } else if(ctype_digit("$options[parent]")) { + $parent = (int) $options['parent']; + } else { + $parent = $this->pages->getByPath($options['parent']); + if(!$parent->id) $parent = null; + } + unset($options['parent']); + } + + // name and parent can be detected from path, when specified + if(!empty($options['path'])) { + $path = trim($options['path'], '/'); + if(strpos($path, '/') === false) $path = "/$path"; + $parts = explode('/', $path); // note index[0] is blank + $name = array_pop($parts); + if(empty($options['name']) && !empty($name)) { + // detect name from path + $options['name'] = $name; + } + if(empty($parent)) { + // detect parent from path + $parentPath = count($parts) ? implode('/', $parts) : '/'; + $parent = $this->pages->getByPath($parentPath); + if(!$parent->id) $parent = null; + } + unset($options['path']); + } + + // detect template from parent (when possible) + if(!$template && !empty($parent)) { + $parent = is_object($parent) ? $parent : $this->pages->get($parent); + if($parent->id) { + if(count($parent->template->childTemplates) === 1) { + $template = $parent->template->childTemplates()->first(); + } + } else { + $parent = null; + } + } + + // detect parent from template (when possible) + if($template && empty($parent) && count($template->parentTemplates) === 1) { + $parentTemplates = $template->parentTemplates(); + if($parentTemplates->count()) { + $numParents = $this->pages->count("template=$parentTemplates, include=all"); + if($numParents === 1) { + $parent = $this->pages->get("template=$parentTemplates"); + } + } + } + + // detect class from template + if(empty($class) && $template) $class = $template->getPageClass(); + + if($parent) $options['parent'] = $parent; + if($template) $options['template'] = $template; + if($class) $options['pageClass'] = $class; + + unset($options['id']); // just in case it was there + + return $options; + } /** * Hook after Fieldtype::sleepValue to remove MB4 characters when present and applicable