diff --git a/CHANGELOG.md b/CHANGELOG.md index ff4b81376..874377b22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ The changelog contains informations about bug fixes, new features or bc breaking ### Added +- [#1227](https://github.com/luyadev/luya/issues/1227) Added preloadModels() method for the Menu Query in order to collect all models for the given request. This can strongly reduce the sql count when working with properties or models. - [#1266](https://github.com/luyadev/luya/issues/1266) render() method for the mailer component in order to provide controller template files. ### Fixed diff --git a/docs/guide/app-cmsproperties.md b/docs/guide/app-cmsproperties.md index b1f835682..03ee1e020 100644 --- a/docs/guide/app-cmsproperties.md +++ b/docs/guide/app-cmsproperties.md @@ -98,6 +98,8 @@ A very common scenario is to add properties to an existing menu item like an ima This method allows you find and evaluate properties for menu items and allows you also to use `Yii::$app->menu->current->getProperty('xyz')`. +> When dealing with large menus you can preload the models (with its properties) for a given menu query use {{luya\cms\menu\Query::preloadModels}} or {{luya\cms\Menu::findAll}} with second statement `true`. + #### in Layouts In the layout you can always get the current propertys based on the current active menu item. diff --git a/modules/cms/src/Menu.php b/modules/cms/src/Menu.php index 1ecbff74c..660807687 100644 --- a/modules/cms/src/Menu.php +++ b/modules/cms/src/Menu.php @@ -447,13 +447,14 @@ public function find() * Wrapper method to get all menu items for the current language without hidden items for * the specific where statement. * - * @param array $where See \luya\cms\menu\Query::where() + * @param array $where See {{\luya\cms\menu\Query::where}} + * @param boolean $preloadModels Whether to preload all models for the given menu Query. See {{luya\cms\menu\Query::preloadModels}} * @see \luya\cms\menu\Query::where() * @return \luya\cms\menu\QueryIterator */ - public function findAll(array $where) + public function findAll(array $where, $preloadModels = false) { - return (new MenuQuery())->where($where)->all(); + return (new MenuQuery())->where($where)->preloadModels($preloadModels)->all(); } /** diff --git a/modules/cms/src/menu/Item.php b/modules/cms/src/menu/Item.php index 48aba0899..ee7849970 100644 --- a/modules/cms/src/menu/Item.php +++ b/modules/cms/src/menu/Item.php @@ -565,6 +565,16 @@ public function getModel() return $this->_model; } + /** + * Setter method for the Model. + * + * @param null|\luya\cms\models\Nav $model The Nav model Active Record + */ + public function setModel($model) + { + $this->_model = $model; + } + /** * This method allows you the retrieve a property for an page property. If the property is not found false will be retunrend * otherwhise the property object itself will be returned (implements `\admin\base\Property`) so you can retrieve the value of the diff --git a/modules/cms/src/menu/Query.php b/modules/cms/src/menu/Query.php index 9df539cb1..f993210dc 100644 --- a/modules/cms/src/menu/Query.php +++ b/modules/cms/src/menu/Query.php @@ -4,6 +4,7 @@ use Yii; use luya\cms\Exception; +use yii\base\Object; /** * Menu Query Builder. @@ -45,21 +46,14 @@ * @since 1.0.0 * @author Basil Suter */ -class Query extends \yii\base\Object +class Query extends Object { - private $_where = []; - - private $_lang = null; - - private $_menu = null; - - private $_whereOperators = ['<', '<=', '>', '>=', '=', '==', 'in']; - - private $_with = ['hidden' => false]; - - private $_offset = null; + /** + * @var array An array with all available where operators. + */ + protected $whereOperators = ['<', '<=', '>', '>=', '=', '==', 'in']; - private $_limit = null; + private $_menu = null; /** * Getter method to return menu component @@ -75,6 +69,8 @@ public function getMenu() return $this->_menu; } + private $_where = []; + /** * Query where similar behavior of filtering items. * @@ -133,7 +129,7 @@ public function getMenu() public function where(array $args) { foreach ($args as $key => $value) { - if (in_array($value, $this->_whereOperators, true)) { + if (in_array($value, $this->whereOperators, true)) { if (count($args) !== 3) { throw new Exception(sprintf("Wrong where(['%s']) condition, see https://luya.io/api/luya-cms-menu-Query#where()-detail for all available conditions.", implode("', '", $args))); } @@ -160,6 +156,8 @@ public function andWhere(array $args) return $this->where($args); } + private $_lang = null; + /** * Changeing the container in where the data should be collection, by default the composition * `langShortCode` is the default language code. This represents the current active language, @@ -175,8 +173,12 @@ public function lang($langShortCode) return $this; } + private $_with = ['hidden' => false]; + /** - * @param string|array $types can be a string containg "hidden" or an array with multiple patters + * With/Without expression to hidde or display data from the Menu Query. + * + * @param string|array $types can be a string containg "hidden" or an array with multiple with statements * for example `['hidden']`. Further with statements upcoming. * @return \luya\cms\menu\Query */ @@ -184,13 +186,31 @@ public function with($types) { $types = (array) $types; foreach ($types as $type) { - if (array_key_exists($type, $this->_with)) { + if (isset($this->_with[$type])) { $this->_with[$type] = true; } } return $this; } + + private $_preloadModels = false; + + /** + * Preload Mmodels for the given Menu Query. + * + * When menu a {{luya\cms\menu\Item::getModel}} method is called it will lazy the given {{luya\cms\models\Nav}} Model. + * This can be slow on large menus, therfore you can preload all models for given Menu Query by enabling this method. + * + * @param boolean $preloadModels Whether to preload all {{luya\cms\menu\Item}} models for {{luya\cms\menu\Item::getModel}} or not. + * @return \luya\cms\menu\Query + */ + public function preloadModels($preloadModels = true) + { + $this->_preloadModels = $preloadModels; + + return $this; + } /** * Return the current language from composition if not set via `lang()`. @@ -206,6 +226,8 @@ public function getLang() return $this->_lang; } + private $_limit = null; + /** * Set a limition for the amount of results. * @@ -221,6 +243,8 @@ public function limit($count) return $this; } + private $_offset = null; + /** * Define offset start for the rows, if you defined offset to be 5 and you have 11 rows, the * first 5 rows will be skiped. This is commonly used to make pagination function in combination @@ -264,7 +288,7 @@ public function one() */ public function all() { - return static::createArrayIterator($this->filter($this->menu[$this->getLang()], $this->_where, $this->_with), $this->getLang(), $this->_with); + return static::createArrayIterator($this->filter($this->menu[$this->getLang()], $this->_where, $this->_with), $this->getLang(), $this->_with, $this->_preloadModels); } /** @@ -283,11 +307,12 @@ public function count() * * @param array $data The filtere results where the iterator object should be created with * @param string $langContext The language short code context, if any. + * @param integer $preloadModels Whether the models should be preload or not. * @return \luya\cms\menu\QueryIterator */ - public static function createArrayIterator(array $data, $langContext, $with) + public static function createArrayIterator(array $data, $langContext, $with, $preloadModels = false) { - return (new QueryIteratorFilter(Yii::createObject(['class' => QueryIterator::className(), 'data' => $data, 'lang' => $langContext, 'with' => $with]))); + return (new QueryIteratorFilter(new QueryIterator(['data' => $data, 'lang' => $langContext, 'with' => $with, 'preloadModels' => $preloadModels]))); } /** @@ -295,12 +320,13 @@ public static function createArrayIterator(array $data, $langContext, $with) * of the QueryIterator class. * * @param array $itemArray The item array data for the object - * @param string $langContext The language short code context, if any. + * @param string $langContext The language short code context, if any. + * @param null|\luya\cms\models\Nav The nav model from the preload stage. * @return \luya\cms\menu\Item */ - public static function createItemObject(array $itemArray, $langContext) + public static function createItemObject(array $itemArray, $langContext, $model = null) { - return Yii::createObject(['class' => Item::className(), 'itemArray' => $itemArray, 'lang' => $langContext]); + return new Item(['itemArray' => $itemArray, 'lang' => $langContext, 'model' => $model]); } /** diff --git a/modules/cms/src/menu/QueryIterator.php b/modules/cms/src/menu/QueryIterator.php index 404e736d0..9c78f32a1 100644 --- a/modules/cms/src/menu/QueryIterator.php +++ b/modules/cms/src/menu/QueryIterator.php @@ -2,7 +2,11 @@ namespace luya\cms\menu; +use Yii; use Iterator; +use yii\base\Object; +use luya\cms\models\Nav; +use luya\helpers\ArrayHelper; /** * Iterator class for menu items. @@ -13,7 +17,7 @@ * * @author Basil Suter */ -class QueryIterator extends \yii\base\Object implements Iterator +class QueryIterator extends Object implements Iterator { /** * @var array An array containing the data to iterate. @@ -31,6 +35,54 @@ class QueryIterator extends \yii\base\Object implements Iterator */ public $with = []; + /** + * @var boolean Whether all models for each menu element should be preloaded or not, on large systems with propertie access it + * can reduce the sql requests but uses more memory instead. + */ + public $preloadModels = false; + + /** + * @internal + */ + public function init() + { + parent::init(); + + if ($this->preloadModels) { + $this->loadModels(); + } + } + + private $_loadModels = null; + + /** + * Load all models for ghe given Menu Query. + * + * + * @return array An array where the key is the id of the nav model and value the {{luya\cms\models\Nav}} object. + */ + public function loadModels() + { + if ($this->_loadModels === null) { + $this->_loadModels = Nav::find()->indexBy('id')->where(['in', 'id', ArrayHelper::getColumn($this->data, 'nav_id')])->with(['properties'])->all(); + } + + return $this->_loadModels; + } + + /** + * Get the model for a given id. + * + * If the model was not preloaded by {{loadModels}} null is returned. + * + * @param integer $id + * @return null|\luya\cms\models\Nav + */ + public function getLoadedModel($id) + { + return isset($this->_loadModels[$id]) ? $this->_loadModels[$id] : null; + } + /** * Iterator get current element, generates a new object for the current item on accessing.s. * @@ -38,7 +90,8 @@ class QueryIterator extends \yii\base\Object implements Iterator */ public function current() { - return Query::createItemObject(current($this->data), $this->lang); + $data = current($this->data); + return Query::createItemObject($data, $this->lang, $this->getLoadedModel($data['id'])); } /** diff --git a/modules/cms/tests/src/menu/QueryTest.php b/modules/cms/tests/src/menu/QueryTest.php index d96685f51..b6bd06c29 100644 --- a/modules/cms/tests/src/menu/QueryTest.php +++ b/modules/cms/tests/src/menu/QueryTest.php @@ -122,4 +122,15 @@ public function testInOperatorWithContainers() $this->assertSame(3, $in); } + + public function testPreloadModels() + { + $default = (new Query())->where(['container' => 'default'])->all(); + + $this->assertNull($default->getInnerIterator()->getLoadedModel(1)); + + $default = (new Query())->where(['container' => 'default'])->preloadModels()->all(); + + $this->assertNotNull($default->getInnerIterator()->getLoadedModel(1)); + } }