From b1b46e4e111e17db16af6c581b47a154760a3e9e Mon Sep 17 00:00:00 2001 From: Ryan Cramer Date: Mon, 13 Apr 2015 14:23:57 -0400 Subject: [PATCH] Attempted fix for issue #1074 and issue #957 where adding language was causing an exception and missing column errors. This commit also contains various unrelated code documentation additions. --- wire/core/DatabaseQuery.php | 13 +- wire/core/Fieldtype.php | 32 +++- wire/core/Inputfield.php | 5 +- wire/core/Page.php | 14 ++ wire/core/PageFinder.php | 33 ++-- wire/core/Pages.php | 160 +++++++++++++++--- wire/core/PagesType.php | 50 +++--- wire/core/User.php | 1 + .../FieldtypeTextLanguage.module | 38 ++--- .../LanguageSupport/LanguageSupport.module | 42 ++++- wire/modules/LanguageSupport/Languages.php | 89 +++++++--- .../Process/ProcessField/ProcessField.module | 2 +- 12 files changed, 355 insertions(+), 124 deletions(-) diff --git a/wire/core/DatabaseQuery.php b/wire/core/DatabaseQuery.php index 3c6e2b55..7d552c77 100644 --- a/wire/core/DatabaseQuery.php +++ b/wire/core/DatabaseQuery.php @@ -16,6 +16,8 @@ * * http://www.processwire.com * http://www.ryancramer.com + * + * @property array $where * */ abstract class DatabaseQuery extends WireData { @@ -56,6 +58,9 @@ public function __get($key) { /** * Merge the contents of current query with another + * + * @param DatabaseQuery $query + * @return this * */ public function merge(DatabaseQuery $query) { @@ -82,7 +87,13 @@ protected function getQueryWhere() { foreach($where as $s) $sql .= "\nAND $s "; return $sql; } - + + /** + * Prepare and return a PDOStatement + * + * @return PDOStatement + * + */ public function prepare() { $query = $this->wire('database')->prepare($this->getQuery()); foreach($this->bindValues as $key => $value) { diff --git a/wire/core/Fieldtype.php b/wire/core/Fieldtype.php index a82b12ec..1bbf5b74 100644 --- a/wire/core/Fieldtype.php +++ b/wire/core/Fieldtype.php @@ -12,6 +12,32 @@ * http://processwire.com * * + * Hookable methods + * ================ + * @method InputfieldWrapper getConfigInputfields(Field $field) + * @method InputfieldWrapper getConfigAdvancedInputfields(Field $field) + * @method array getConfigAllowContext(Field $field) + * @method array exportConfigData(Field $field, array $data) + * @method array importConfigData(Field $field, array $data) + * @method Fieldtypes|null getCompatibleFieldtypes(Field $field) + * @method mixed formatValue(Page $page, Field $field, $value) + * @method string|MarkupFieldtype markupValue(Page $page, Field $field, $value = null, $property = '') + * @method mixed wakeupValue(Page $page, Field $field, $value) + * @method string|int|array sleepValue(Page $page, Field $field, $value) + * @method string|float|int|array exportValue(Page $page, Field $field, $value, array $options = array()) + * @method bool createField(Field $field) + * @method array getSelectorInfo(Field $field, array $data = array()) + * @method mixed|null loadPageField(Page $page, Field $field) + * @method bool savePageField(Page $page, Field $field) + * @method bool deleteField(Field $field) + * @method bool deletePageField(Page $page, Field $field) + * @method bool emptyPageField(Page $page, Field $field) + * @method bool replacePageField(Page $src, Page $dst, Field $field) + * @method bool deleteTemplateField(Template $template, Field $field) + * @method Field cloneField(Field $field) + * @method void install() + * @method void uninstall() + * */ abstract class Fieldtype extends WireData implements Module { @@ -620,13 +646,13 @@ public function ___loadPageField(Page $page, Field $field) { if($isMulti) $query->orderby('sort'); $value = null; - $stmt = $query->prepare(); try { - $stmt->execute(); + $stmt = $query->prepare(); + $result = $this->wire('pages')->executeQuery($stmt); } catch(Exception $e) { + $result = false; $this->error($e->getMessage()); } - $result = $stmt->errorCode() > 0 ? false : true; $fieldName = $database->escapeCol($field->name); $schema = $this->trimDatabaseSchema($this->getDatabaseSchema($field)); diff --git a/wire/core/Inputfield.php b/wire/core/Inputfield.php index 38cbc833..04b873a1 100644 --- a/wire/core/Inputfield.php +++ b/wire/core/Inputfield.php @@ -63,8 +63,9 @@ interface InputfieldHasArrayValue { } * @property string $headerClass Optional class name (CSS) to apply to the InputfieldHeader element * @property string $contentClass Optional class name (CSS) to apply to the InputfieldContent element * @property InputfieldWrapper|null $parent The parent InputfieldWrapper for this Inputfield or null if not set. - * @property null|Fieldtype hasFieldtype Set to the Fieldtype using this Inputfield (by Field), when applicable, null when not. - * @property null|bool entityEncodeLabel Set to boolean false to specifically disable entity encoding of field header/label. + * @property null|Fieldtype $hasFieldtype Set to the Fieldtype using this Inputfield (by Field), when applicable, null when not. + * @property null|bool $entityEncodeLabel Set to boolean false to specifically disable entity encoding of field header/label. + * @property bool|null $useLanguages When multi-language support active, can be set to true to make it provide inputs for each language (where supported). * * * diff --git a/wire/core/Page.php b/wire/core/Page.php index cf991dac..f674497d 100644 --- a/wire/core/Page.php +++ b/wire/core/Page.php @@ -38,7 +38,9 @@ * @property Page $prev This page's previous sibling page, or NullPage if it is the first sibling. See also $page->prev($pageArray). * @property string $created Unix timestamp of when the page was created * @property string $modified Unix timestamp of when the page was last modified + * @property int $created_users_id ID of created user * @property User $createdUser The user that created this page. Returns a User or a NullUser. + * @property int $modified_users_id ID of last modified user * @property User $modifiedUser The user that last modified this page. Returns a User or a NullUser. * @property PagefilesManager $filesManager * @property bool $outputFormatting Whether output formatting is enabled or not. @@ -48,7 +50,9 @@ * @property int $sort Sort order of this page relative to siblings (applicable when manual sorting is used). * @property string $sortfield Field that a page is sorted by relative to its siblings (default=sort, which means drag/drop manual) * @property null|array _statusCorruptedFields Field names that caused the page to have Page::statusCorrupted status. + * @property int $status Page status flags * @property string statusStr Returns space-separated string of status names active on this page. + * @property Fieldgroup $fieldgroup Shorter alias for $page->template->fieldgroup * * Methods added by PageRender.module: * ----------------------------------- @@ -70,6 +74,14 @@ * ------------------------------------------------------------------ * @method Page setLanguageValue($language, $fieldName, $value) Set value for field in language (requires LanguageSupport module). $language may be ID, language name or Language object. * @method Page getLanguageValue($language, $fieldName) Get value for field in language (requires LanguageSupport module). $language may be ID, language name or Language object. + * + * Hookable methods + * ---------------- + * @method mixed getUnknown($key) Last stop to find a property that we haven't been able to locate. + * @method Page rootParent() Get parent closest to homepage. + * @method void loaded() Called when page is loaded. + * @method void setEditor(WirePageEditor $editor) + * @method string getIcon() * */ @@ -1263,6 +1275,8 @@ public function prevUntil($selector = '', $filter = '', PageArray $siblings = nu * * @param Field|string $field Optional field to save (name of field or Field object) * @param array $options See Pages::save for options. You may also specify $options as the first argument if no $field is needed. + * @return bool true on success false on fail + * @throws WireException on database error * */ public function save($field = null, array $options = array()) { diff --git a/wire/core/PageFinder.php b/wire/core/PageFinder.php index 8ad12b82..4c4bddc6 100644 --- a/wire/core/PageFinder.php +++ b/wire/core/PageFinder.php @@ -123,6 +123,9 @@ public function __construct() { /** * Pre-process the selectors to add Page status checks + * + * @param Selectors $selectors + * @param array $options * */ protected function setupStatusChecks(Selectors $selectors, array &$options) { @@ -248,6 +251,11 @@ protected function setupStatusChecks(Selectors $selectors, array &$options) { /** * Return all pages matching the given selector. + * + * @param Selectors $selectors + * @param array $options + * @return array + * @throws PageFinderException * */ public function ___find(Selectors $selectors, $options = array()) { @@ -278,23 +286,12 @@ public function ___find(Selectors $selectors, $options = array()) { try { $stmt = $query->prepare(); - $stmt->execute(); + $this->wire('pages')->executeQuery($stmt); $error = ''; } catch(Exception $e) { $error = $e->getMessage(); } - if($stmt->errorCode() > 0) { - $errorInfo = $stmt->errorInfo(); - $error = $errorInfo[2] . ($error ? " - $error" : ""); - if($stmt->errorCode() == '42S22') { - // unknown column - if(preg_match('/[\'"]([_a-z0-9]+\.[_a-z0-9]+)[\'"]/i', $errorInfo[2], $matches)) { - $this->unknownColumnError($matches[1], $errorInfo[2], $query); - } - } - } - if($error) { $this->log($error); throw new PageFinderException($error); @@ -345,12 +342,6 @@ public function ___find(Selectors $selectors, $options = array()) { return $matches; } - protected function ___unknownColumnError($column, $error, DatabaseQuery $query) { - if($this->wire('languages')) { - $this->wire('languages')->unknownColumnError($column); - } - } - /** * Same as find() but returns just a simple array of page IDs without any other info * @@ -1153,6 +1144,7 @@ protected function ___getQueryJoinPath(DatabaseQuerySelect $query, $selector) { if($this->modules->isInstalled('PagePaths') && !$langNames) { // @todo add support to PagePaths module for LanguageSupportPageNames $pagePaths = $this->modules->get('PagePaths'); + /** @var PagePaths $pagePaths */ $pagePaths->getMatchQuery($query, $selector); return; } @@ -1203,6 +1195,11 @@ protected function ___getQueryJoinPath(DatabaseQuerySelect $query, $selector) { * Special case when field is native to the pages table * * TODO not all operators will work here, so may want to add some translation or filtering + * + * @param DatabaseQuerySelect $query + * @param Selector $selector + * @param array $fields + * @throws PageFinderSyntaxException * */ protected function getQueryNativeField(DatabaseQuerySelect $query, $selector, $fields) { diff --git a/wire/core/Pages.php b/wire/core/Pages.php index 80e428a1..979bb30b 100644 --- a/wire/core/Pages.php +++ b/wire/core/Pages.php @@ -8,10 +8,8 @@ * * This is the most used object in the ProcessWire API. * - * @TODO Move everything into delegate classes, leaving this as just the interface to them. - * * ProcessWire 2.x - * Copyright (C) 2013 by Ryan Cramer + * Copyright (C) 2015 by Ryan Cramer * Licensed under GNU/GPL v2, see LICENSE.TXT * * http://processwire.com @@ -20,13 +18,14 @@ * @link http://processwire.com/api/variables/pages/ Offical $pages Documentation * @link http://processwire.com/api/selectors/ Official Selectors Documentation * - * @method PageArray find() find($selectorString, array $options) Find and return all pages matching the given selector string. Returns a PageArray. + * @method PageArray find() find($selectorString, array $options = array()) Find and return all pages matching the given selector string. Returns a PageArray. * @method bool save() save(Page $page) Save any changes made to the given $page. Same as : $page->save() Returns true on success * @method bool saveField() saveField(Page $page, $field) Save just the named field from $page. Same as : $page->save('field') * @method bool trash() trash(Page $page, $save = true) Move a page to the trash. If you have already set the parent to somewhere in the trash, then this method won't attempt to set it again. * @method bool delete() delete(Page $page, $recursive = false) Permanently delete a page and it's fields. Unlike trash(), pages deleted here are not restorable. If you attempt to delete a page with children, and don't specifically set the $recursive param to True, then this method will throw an exception. If a recursive delete fails for any reason, an exception will be thrown. * @method Page|NullPage clone() clone(Page $page, Page $parent = null, $recursive = true, $options = array()) Clone an entire page, it's assets and children and return it. - * + * + * @todo Some methods here (find, save, etc.) need their own dedicated classes. * @todo Update saveField to accept array of field names as an option. * */ @@ -91,6 +90,14 @@ class Pages extends Wire { */ protected $cloning = false; + /** + * Autojoin allowed? + * + * @var bool + * + */ + protected $autojoin = true; + /** * Name for autogenerated page names when fields to generate name aren't populated * @@ -131,7 +138,7 @@ public function init() { * - caller: string - optional name of calling function, for debugging purposes, i.e. pages.count * - include: string - Optional inclusion mode of 'hidden', 'unpublished' or 'all'. Default=none. Typically you would specify this * directly in the selector string, so the option is mainly useful if your first argument is not a string. - * - loadOptions: array - Optional assoc array of options to pass to getById() load options . + * - loadOptions: array - Optional assoc array of options to pass to getById() load options. * @return PageArray * */ @@ -294,7 +301,11 @@ public function findOne($selectorString, $options = array()) { if(empty($selectorString)) return new NullPage(); if($page = $this->getCache($selectorString)) return $page; if(is_string($options)) $options = Selectors::keyValueStringToArray($options); - $defaults = array('findOne' => true, 'getTotal' => false, 'caller' => 'pages.get'); + $defaults = array( + 'findOne' => true, + 'getTotal' => false, + 'caller' => 'pages.get' + ); $options = array_merge($defaults, $options); $page = $this->find($selectorString, $options)->first(); if(!$page) $page = new NullPage(); @@ -374,7 +385,7 @@ public function getById($_ids, $template = null, $parent_id = null) { } else if(!is_null($template) && !$template instanceof Template) { throw new WireException('getById argument 2 must be Template or $options array'); } - + $pageArrayClass = $options['pageArrayClass']; if(!is_null($parent_id) && !is_int($parent_id)) { @@ -452,7 +463,7 @@ public function getById($_ids, $template = null, $parent_id = null) { } $query = $database->prepare($sql); - $result = $query->execute(); + $result = $this->executeQuery($query); if($result) while($row = $query->fetch(PDO::FETCH_NUM)) { list($id, $templates_id) = $row; $id = (int) $id; @@ -497,7 +508,7 @@ public function getById($_ids, $template = null, $parent_id = null) { if($joinSortfield) $query->leftjoin('pages_sortfields ON pages_sortfields.pages_id=pages.id'); $query->groupby('pages.id'); - if($options['autojoin']) foreach($fields as $field) { + if($options['autojoin'] && $this->autojoin) foreach($fields as $field) { if(!empty($options['joinFields']) && in_array($field->name, $options['joinFields'])) { // joinFields option specified to force autojoin this field } else { @@ -515,11 +526,8 @@ public function getById($_ids, $template = null, $parent_id = null) { $query->where("pages.id IN(" . implode(',', $ids) . ") "); // QA $query->from("pages"); - $stmt = $query->execute(); - if($stmt->errorCode() > 0) { - $errorInfo = $result->errorInfo(); - throw new WireException($errorInfo[2]); - } + $stmt = $query->prepare(); + $this->executeQuery($stmt); $class = $options['pageClass']; if(empty($class)) { @@ -687,7 +695,7 @@ public function _path($id) { do { $query = $database->prepare("SELECT parent_id, name FROM pages WHERE id=:parent_id"); // QA $query->bindValue(":parent_id", (int) $parent_id, PDO::PARAM_INT); - $query->execute(); + $this->executeQuery($query); list($parent_id, $name) = $query->fetch(PDO::FETCH_NUM); $path = $name . '/' . $path; } while($parent_id > 1); @@ -1094,7 +1102,7 @@ protected function savePageQuery(Page $page, array $options) { try { $result = false; - $result = $query->execute(); + $result = $this->executeQuery($query); } catch(Exception $e) { @@ -1288,7 +1296,7 @@ public function ___saveField(Page $page, $field, $options = array()) { $query = $database->prepare("UPDATE pages SET modified_users_id=:userID, modified=NOW() WHERE id=:pageID"); $query->bindValue(':userID', $userID, PDO::PARAM_INT); $query->bindValue(':pageID', $page->id, PDO::PARAM_INT); - $query->execute(); + $this->executeQuery($query); } $return = true; $this->savedField($page, $field); @@ -1355,7 +1363,7 @@ protected function saveParents($pages_id, $numChildren, $level = 0) { $query = $database->prepare($sql); $query->bindValue(':pages_id', $pages_id, PDO::PARAM_INT); - $query->execute(); + $this->executeQuery($query); while($row = $query->fetch(PDO::FETCH_ASSOC)) { $this->saveParents($row['id'], $row['numChildren'], $level+1); @@ -1385,12 +1393,12 @@ protected function savePageStatus($pageID, $status, $recursive = false, $remove $query = $database->prepare("UPDATE pages SET status=$sql WHERE id=:page_id"); $query->bindValue(":page_id", $pageID, PDO::PARAM_INT); - $query->execute(); + $this->executeQuery($query); if($recursive) { $query = $database->prepare("SELECT id FROM pages WHERE parent_id=:parent_id"); // QA $query->bindValue(":parent_id", $pageID, PDO::PARAM_INT); - $query->execute(); + $this->executeQuery($query); while($row = $query->fetch(PDO::FETCH_ASSOC)) { $this->savePageStatus($row['id'], $status, true, $remove); } @@ -1492,6 +1500,7 @@ public function ___delete(Page $page, $recursive = false, array $options = array if(!$recursive) { throw new WireException("Can't delete Page $page because it has one or more children."); } else foreach($page->children("include=all") as $child) { + /** @var Page $child */ if(!$this->delete($child, true)) throw new WireException("Error doing recursive page delete, stopped by page $child"); } } @@ -1617,6 +1626,7 @@ public function ___clone(Page $page, Page $parent = null, $recursive = true, $op // if there are children, then recurisvely clone them too if($page->numChildren && $recursive) { foreach($page->children("include=all") as $child) { + /** @var Page $child */ $this->clone($child, $copy); } } @@ -1838,6 +1848,99 @@ public function getPageFinder() { return new PageFinder(); } + /** + * Enable or disable use of autojoin for all queries + * + * Default should always be true, and you may use this to turn it off temporarily, but + * you should remember to turn it back on + * + * @param bool $autojoin + * + */ + public function setAutojoin($autojoin = true) { + $this->autojoin = $autojoin ? true : false; + } + + /** + * Execute a PDO statement, with additional error handling + * + * @param PDOStatement $query + * @param bool $throw Whether or not to throw exception on query error (default=true) + * @param int $maxTries Max number of times it will attempt to retry query on error + * @return bool + * + */ + public function executeQuery(PDOStatement $query, $throw = true, $maxTries = 3) { + + $tryAgain = 0; + $_throw = $throw; + + do { + try { + $result = $query->execute(); + + } catch(PDOException $e) { + + $result = false; + $error = $e->getMessage(); + $throw = false; // temporarily disable while we try more + + if($tryAgain === 0) { + // setup retry loop + $tryAgain = $maxTries; + } else { + // decrement retry loop + $tryAgain--; + } + + if($tryAgain < 1) { + // if at end of retry loop, restore original throw state + $throw = $_throw; + } + + if(stripos($error, 'MySQL server has gone away') !== false) { + // forces reconection on next query + $this->wire('database')->closeConnection(); + + } else if($query->errorCode() == '42S22') { + // unknown column error + $errorInfo = $query->errorInfo(); + if(preg_match('/[\'"]([_a-z0-9]+\.[_a-z0-9]+)[\'"]/i', $errorInfo[2], $matches)) { + $this->unknownColumnError($matches[1]); + } + + } else { + // some other error that we don't have retry plans for + // tryAgain=0 will force the loop to stop + $tryAgain = 0; + } + + if($throw) { + throw $e; + } else { + $this->error($error); + } + } + + } while($tryAgain && !$result); + + return $result; + } + + /** + * Called when a page-data loading query encounters an unknown column + * + * @param string $column Column informat tableName.columnName + * + */ + protected function ___unknownColumnError($column) { + if($this->wire('modules')->isInstalled('LanguageSupport')) { + if(!$this->wire('languages')) $this->wire('modules')->get('LanguageSupport'); + } + if($this->wire('languages')) { + $this->wire('languages')->unknownColumnError($column); + } + } /** * Enables use of $pages(123), $pages('/path/') or $pages('selector string') @@ -1893,6 +1996,8 @@ public function ___saved(Page $page, array $changes = array(), $values = array() /** * Hook called when a new page has been added + * + * @param Page $page * */ public function ___added(Page $page) { @@ -1903,6 +2008,8 @@ public function ___added(Page $page) { * Hook called when a page has been moved from one parent to another * * Note the previous parent is in $page->parentPrevious + * + * @param Page $page * */ public function ___moved(Page $page) { @@ -1917,6 +2024,8 @@ public function ___moved(Page $page) { * Hook called when a page's template has been changed * * Note the previous template is in $page->templatePrevious + * + * @param Page $page * */ public function ___templateChanged(Page $page) { @@ -1929,6 +2038,8 @@ public function ___templateChanged(Page $page) { /** * Hook called when a page has been moved to the trash + * + * @param Page $page * */ public function ___trashed(Page $page) { @@ -1937,6 +2048,8 @@ public function ___trashed(Page $page) { /** * Hook called when a page has been moved OUT of the trash + * + * @param Page $page * */ public function ___restored(Page $page) { @@ -1961,12 +2074,16 @@ public function ___saveReady(Page $page) { return array(); } * * This is different from a before(delete) hook because this hook is called once it has * been confirmed that the page is deleteable and WILL be deleted. + * + * @param Page $page * */ public function ___deleteReady(Page $page) { } /** * Hook called when a page and it's data have been deleted + * + * @param Page $page * */ public function ___deleted(Page $page) { @@ -2131,7 +2248,6 @@ public function ___savedField(Page $page, Field $field) { $this->log("Saved page field '$field->name'", $page); } - } diff --git a/wire/core/PagesType.php b/wire/core/PagesType.php index b6687737..ed4310e7 100644 --- a/wire/core/PagesType.php +++ b/wire/core/PagesType.php @@ -111,7 +111,7 @@ public function addParents($parents) { } else if(is_string($parent) && ctype_digit($parent)) { $id = (int) $parent; } else if(is_string($parent)) { - $parent = $this->wire('pages')->get($parent); + $parent = $this->wire('pages')->findOne($parent, array('loadOptions' => array('autojoin' => false))); $id = $parent->id; } else if(is_object($parent) && $parent instanceof Page) { $id = $parent->id; @@ -190,13 +190,31 @@ public function isValid(Page $page) { if(!$validParent && count($this->parents)) { $validParents = impode(', ', $this->parents); - $this->error("Page $page->path must have parent: $validParents"); + $this->error("Page $page->path must have parent: $validParents"); return false; } return true; } + /** + * Get options that will be passed to Pages::getById() + * + * @param array $loadOptions Optionally specify options to merge with and override defaults + * @return array + * + */ + protected function getLoadOptions(array $loadOptions = array()) { + $_loadOptions = array( + 'pageClass' => $this->getPageClass(), + //'getNumChildren' => false, + 'joinSortfield' => false, + 'joinFields' => $this->getJoinFieldNames() + ); + if(count($loadOptions)) $_loadOptions = array_merge($_loadOptions, $loadOptions); + return $_loadOptions; + } + /** * Given a Selector string, return the Page objects that match in a PageArray. * @@ -207,14 +225,10 @@ public function isValid(Page $page) { * */ public function find($selectorString, $options = array()) { - if(!isset($options['findAll'])) $options['findAll'] = true; - if(!isset($options['loadOptions'])) $options['loadOptions'] = array( - 'pageClass' => $this->getPageClass(), - //'getNumChildren' => false, - 'joinSortfield' => false, - 'joinFields' => $this->getJoinFieldNames(), - ); - $pages = $this->pages->find($this->selectorString($selectorString), $options); + if(!isset($options['findAll'])) $options['findAll'] = true; + if(!isset($options['loadOptions'])) $options['loadOptions'] = array(); + $options['loadOptions'] = $this->getLoadOptions($options['loadOptions']); + $pages = $this->wire('pages')->find($this->selectorString($selectorString), $options); foreach($pages as $page) $this->loaded($page); return $pages; } @@ -229,16 +243,10 @@ public function find($selectorString, $options = array()) { */ public function get($selectorString) { - $options = array( - 'getOne' => true, - 'pageClass' => $this->getPageClass(), - //'getNumChildren' => false, - 'joinSortfield' => false, - 'joinFields' => $this->getJoinFieldNames(), - ); + $options = $this->getLoadOptions(array('getOne' => true)); if(ctype_digit("$selectorString")) { - // selector string contians a page ID + // selector string contains a page ID if(count($this->templates) == 1 && count($this->parents) == 1) { // optimization for when there is only 1 template and 1 parent $options['template'] = $this->template; @@ -265,7 +273,7 @@ public function get($selectorString) { // selector string with operators, can pass through } - $page = $this->pages->get($this->selectorString($selectorString)); + $page = $this->pages->findOne($this->selectorString($selectorString), array('loadOptions' => $options)); if($page->id) $this->loaded($page); return $page; @@ -322,7 +330,7 @@ public function ___delete(Page $page, $recursive = false) { public function ___add($name) { $className = $this->getPageClass(); - $parent = $this->pages->get($this->parent_id); + $parent = $this->getParent(); $page = new $className(); $page->template = $this->template; @@ -367,7 +375,7 @@ public function getParentIDs() { } public function getParent() { - return $this->wire('pages')->get($this->parent_id); + return $this->wire('pages')->findOne($this->parent_id); } public function getParents() { diff --git a/wire/core/User.php b/wire/core/User.php index bec97de4..6f2072aa 100644 --- a/wire/core/User.php +++ b/wire/core/User.php @@ -17,6 +17,7 @@ * @property string email Get or set email address for this user. * @property string pass Set the user's password. Note that when getting, this returns a hashed version of the password, so it is not typically useful to get this property. However, it is useful to set this property if you want to change the password. When you change a password, it is assumed to be the non-hashed/non-encrypted version. ProcessWire will hash it automatically when the user is saved. * @property PageArray roles Get roles this user has. Returns PageArray. + * @property Language $language User language, applicable only if LanguageSupport installed. * */ diff --git a/wire/modules/LanguageSupport/FieldtypeTextLanguage.module b/wire/modules/LanguageSupport/FieldtypeTextLanguage.module index 4d4c5735..d4c77894 100644 --- a/wire/modules/LanguageSupport/FieldtypeTextLanguage.module +++ b/wire/modules/LanguageSupport/FieldtypeTextLanguage.module @@ -43,26 +43,26 @@ class FieldtypeTextLanguage extends FieldtypeText implements FieldtypeLanguageIn return $value; } - /** - * Return the database schema in specified format - * - */ - public function getDatabaseSchema(Field $field) { - - $schema = parent::getDatabaseSchema($field); - $languageSupport = wire('modules')->get('LanguageSupport'); + /** + * Return the database schema in specified format + * + */ + public function getDatabaseSchema(Field $field) { - // note that we use otherLanguagePageIDs rather than wire('languages') because - // it's possible that this method may be called before the languages are known - foreach($languageSupport->otherLanguagePageIDs as $languageID) { - // $schema['data' . $languageID] = $schema['data']; - $schema['data' . $languageID] = 'text'; - $schema['keys']["data_exact{$languageID}"] = "KEY `data_exact{$languageID}` (`data{$languageID}`(255))"; - $schema['keys']["data{$languageID}"] = "FULLTEXT KEY `data{$languageID}` (`data{$languageID}`)"; - } - - return $schema; - } + $schema = parent::getDatabaseSchema($field); + $languageSupport = wire('modules')->get('LanguageSupport'); + + // note that we use otherLanguagePageIDs rather than wire('languages') because + // it's possible that this method may be called before the languages are known + foreach($languageSupport->otherLanguagePageIDs as $languageID) { + // $schema['data' . $languageID] = $schema['data']; + $schema['data' . $languageID] = 'text'; + $schema['keys']["data_exact{$languageID}"] = "KEY `data_exact{$languageID}` (`data{$languageID}`(255))"; + $schema['keys']["data{$languageID}"] = "FULLTEXT KEY `data{$languageID}` (`data{$languageID}`)"; + } + + return $schema; + } /** * Format value for output, basically typcasting to a string and sending to textformatters from FieldtypeText diff --git a/wire/modules/LanguageSupport/LanguageSupport.module b/wire/modules/LanguageSupport/LanguageSupport.module index bad42058..8529f90f 100644 --- a/wire/modules/LanguageSupport/LanguageSupport.module +++ b/wire/modules/LanguageSupport/LanguageSupport.module @@ -11,10 +11,10 @@ * * http://processwire.com * -$* @property int $languagesPageID -$* @property int $defaultLanguagePageID -$* @property int $languageTranslatorPageID -$* @property array $otherLanguagePageIDs Quick reference to non-default language IDs, for when needed before languages loaded + * @property int $languagesPageID + * @property int $defaultLanguagePageID + * @property int $languageTranslatorPageID + * @property array $otherLanguagePageIDs Quick reference to non-default language IDs, for when needed before languages loaded * */ @@ -59,24 +59,32 @@ class LanguageSupport extends WireData implements Module, ConfigurableModule { /** * Reference to the default language page + * + * @var Language|null * */ protected $defaultLanguagePage = null; /** * Array of pages that were cached before this module was loaded. + * + * @var array * */ protected $earlyCachedPages = array(); /** * Instanceof LanguageSupportFields, if installed + * + * @var LanguageSupportFields|null * */ protected $LanguageSupportFields = null; /** * Instanceof LanguageTabs, if installed + * + * @var LanguageTabs|null * */ protected $languageTabs = null; @@ -121,7 +129,7 @@ class LanguageSupport extends WireData implements Module, ConfigurableModule { // prevent possible double init if($this->initialized) return; - $this->initialized = true; + $this->initialized = true; FieldtypePageTitle::$languageSupport = true; @@ -130,6 +138,10 @@ class LanguageSupport extends WireData implements Module, ConfigurableModule { // create the $languages API var $languageTemplate = $this->templates->get('language'); if(!$languageTemplate) return; + + // prevent fields like 'title' from autojoining until languages are fully loaded + $this->wire('pages')->setAutojoin(false); + $languages = new Languages($languageTemplate, $this->languagesPageID); $_default = null; // just in case @@ -146,12 +158,17 @@ class LanguageSupport extends WireData implements Module, ConfigurableModule { } if(!$this->defaultLanguagePage) { - if($_default) $this->defaultLanguagePage = $_default; - else $this->defaultLanguagePage = $languages->getAll()->first(); + if($_default) { + $this->defaultLanguagePage = $_default; + } else { + $this->defaultLanguagePage = $languages->getAll()->first(); + } } $this->defaultLanguagePage->setIsDefaultLanguage(); $languages->setDefault($this->defaultLanguagePage); - Wire::setFuel('languages', $languages); + + // set $languages API variable + $this->wire('languages', $languages); // identify the current language from the user, or set one if it's not already if($this->user->language && $this->user->language->id) { @@ -161,7 +178,7 @@ class LanguageSupport extends WireData implements Module, ConfigurableModule { $this->user->language = $language; } - wire('config')->dateFormat = $this->_('Y-m-d H:i:s'); // Sortable date format used in the admin + $this->wire('config')->dateFormat = $this->_('Y-m-d H:i:s'); // Sortable date format used in the admin $locale = $this->_('C'); // Value to pass to PHP's setlocale(LC_ALL, 'value') function when initializing this language // Default is 'C'. Specify '0' to skip the setlocale() call (and carry on system default). if($locale != '0') setlocale(LC_ALL, $locale); @@ -184,6 +201,9 @@ class LanguageSupport extends WireData implements Module, ConfigurableModule { if($numOtherLanguages && $numOtherLanguages != count($this->otherLanguagePageIDs)) { $this->refreshLanguageIDs(); } + + // restore autojoin state for pages + $this->wire('pages')->setAutojoin(true); } /** @@ -283,8 +303,10 @@ class LanguageSupport extends WireData implements Module, ConfigurableModule { public function hookInputfieldAfterRender(HookEvent $event) { static $numLanguages = null; + /** @var Inputfield $inputfield */ $inputfield = $event->object; $name = $inputfield->attr('name'); + /** @var Languages $languages */ $languages = $this->wire('languages'); if(is_null($numLanguages)) $numLanguages = $languages->count(); @@ -372,6 +394,7 @@ class LanguageSupport extends WireData implements Module, ConfigurableModule { public function hookInputfieldAfterProcessInput(HookEvent $event) { $inputfield = $event->object; + /** @var Inputfield $inputfield */ if(!$inputfield->useLanguages) return; $post = $event->arguments[0]; $languages = $this->wire('languages'); @@ -420,6 +443,7 @@ class LanguageSupport extends WireData implements Module, ConfigurableModule { $language = $this->wire('user')->language; if(!$language || !$language->id) return; + /** @var Field $field */ $field = $event->object; $inputfield = $event->return; $translatable = array('label', 'description', 'notes'); diff --git a/wire/modules/LanguageSupport/Languages.php b/wire/modules/LanguageSupport/Languages.php index 6c436e88..a7d77cf3 100644 --- a/wire/modules/LanguageSupport/Languages.php +++ b/wire/modules/LanguageSupport/Languages.php @@ -60,6 +60,20 @@ public function translator(Language $language) { else $this->translator->setCurrentLanguage($language); return $this->translator; } + + public function getPageClass() { + return 'Language'; + } + + public function getLoadOptions(array $loadOptions = array()) { + $loadOptions = parent::getLoadOptions($loadOptions); + $loadOptions['autojoin'] = false; + return $loadOptions; + } + + public function getJoinFieldNames() { + return array(); + } /** * Returns ALL languages, including those in the trash or unpublished, etc. (inactive) @@ -71,8 +85,13 @@ public function getAll() { if($this->languagesAll) return $this->languagesAll; $template = $this->getTemplate(); $parent_id = $this->getParentID(); - $this->languagesAll = $this->wire('pages')->find("parent_id=$parent_id, template=$template, include=all"); - return $this->languagesAll; + $selector = "parent_id=$parent_id, template=$template, include=all"; + $languagesAll = $this->wire('pages')->find($selector, array( + 'loadOptions' => $this->getLoadOptions(), + ) + ); + if(count($languagesAll)) $this->languagesAll = $languagesAll; + return $languagesAll; } /** @@ -81,12 +100,13 @@ public function getAll() { */ public function getIterator() { if($this->languages) return $this->languages; - $this->languages = new PageArray(); + $languages = new PageArray(); foreach($this->getAll() as $language) { if($language->is(Page::statusUnpublished) || $language->is(Page::statusHidden)) continue; - $this->languages->add($language); + $languages->add($language); } - return $this->languages; + if(count($languages)) $this->languages = $languages; + return $languages; } /** @@ -173,48 +193,61 @@ public function ___updated(Page $language, $what) { public function reloadLanguages() { $this->languages = null; $this->languagesAll = null; - $this->getIterator(); + } + + public function getParent() { + return $this->wire('pages')->findOne($this->parent_id, array('loadOptions' => array('autojoin' => false))); + } + + public function getParents() { + if(count($this->parents)) { + return $this->wire('pages')->getById($this->parents, array('autojoin' => false)); + } else { + return parent::getParents(); + } } /** - * PageFinder calls this when it catches an unknown column exception - * - * Provides QA to make sure any language-related columns are property setup in case - * something failed during the initial setup process. - * - * This is only here to repair existing installs that were missing a field for one reason or another. - * This method (and the call to it in PageFinder) can eventually be removed. - * + * Pages calls this when it catches an unknown column exception + * + * Provides QA to make sure any language-related columns are property setup in case + * something failed during the initial setup process. + * + * This is only here to repair existing installs that were missing a field for one reason or another. + * This method (and the call to it in Pages) can eventually be removed (?) + * * @param $column - * + * */ public function ___unknownColumnError($column) { - + if(!preg_match('/^([^.]+)\.([^.\d]+)(\d+)$/', $column, $matches)) { return; } - + $table = $matches[1]; - $col = $matches[2]; - $languageID = (int) $matches[3]; - - foreach($this as $language) { + $col = $matches[2]; + $languageID = (int) $matches[3]; + + foreach($this->wire('languages') as $language) { if($language->id == $languageID) { - echo "language $language->name is missing column $column"; + $this->warning("language $language->name is missing column $column", Notice::debug); if($table == 'pages' && $this->wire('modules')->isInstalled('LanguageSupportPageNames')) { - $module = $this->wire('modules')->get('LanguageSupportPageNames'); - $module->languageAdded($language); + $module = $this->wire('modules')->get('LanguageSupportPageNames'); + $module->languageAdded($language); } else if(strpos($table, 'field_') === 0) { - $fieldName = substr($table, strpos($table, '_')+1); - $field = $this->wire('fields')->get($fieldName); + $fieldName = substr($table, strpos($table, '_')+1); + $field = $this->wire('fields')->get($fieldName); if($field && $this->wire('modules')->isInstalled('LanguageSupportFields')) { - $module = $this->wire('modules')->get('LanguageSupportFields'); - $module->fieldLanguageAdded($field, $language); + $module = $this->wire('modules')->get('LanguageSupportFields'); + $module->fieldLanguageAdded($field, $language); } } } } } + + } diff --git a/wire/modules/Process/ProcessField/ProcessField.module b/wire/modules/Process/ProcessField/ProcessField.module index c8290bef..bd154f91 100644 --- a/wire/modules/Process/ProcessField/ProcessField.module +++ b/wire/modules/Process/ProcessField/ProcessField.module @@ -104,7 +104,7 @@ class ProcessField extends Process implements ConfigurableModule { $fieldsArray = array(); $showAll = $this->wire('config')->advanced; foreach($this->wire('fields') as $field) { - if(!$showAll && ($field->flags & Field::flagSystem)) continue; + if(!$showAll && ($field->flags & Field::flagSystem) && $field->name != 'title') continue; $fieldsArray[] = $field; } $options['items'] = $fieldsArray;