I am building out a nested menu for a Magento store I am working on. The store has around 700 categories in total (that are nested around 4 levels at most) that need to be spat out into this menu.
The code I have written takes on average 2.5s to process (tested using microtime).
I am wondering if this is unavoidable given the amount of categories that need to be processed.
Anyways, this is the code I have come up with (go easy I am a front end dev by trade): NOTE: is am also using this code to loop out CMS pages in the same fashion
$type = Mage::registry('current_category') ? 'category' : 'page';
if($type == 'category') {
$currentID = Mage::registry('current_category')->getId();
$parentIDs = explode('/', Mage::registry('current_category')->path);
$rootID = Mage::app()->getStore()->getRootCategoryId();
}
else {
$currentID = Mage::getSingleton('cms/page')->getId();
$parentIDs = Mage::getSingleton('cms/page')->getPathIds();
$rootID = 0;
}
function checkChildHtml($parentId, $htmlString) {
$string = '';
if($parentId != $rootID) {
$string = $htmlString;
}
return $string;
}
// Recurse the site tree and build out a menu
function buildChildMenu($type, $currentID, $parentId, $isChild, $parentIDs, $rootID) {
// Get the appropriate collection based on type
if($type == 'category') {
$children = Mage::getModel('catalog/category')->getCollection()
->addAttributeToSelect('*')
->addAttributeToFilter('is_active', '1')
->addAttributeToFilter('include_in_menu', '1')
->addAttributeToFilter('parent_id', array('eq' => $parentId))
->addAttributeToSort('position', 'asc');
}
else {
$children = Mage::getModel('cms/page')->getCollection()
->addFieldToSelect('*')
->addFieldToFilter('is_active', '1')
->addFieldToFilter('include_in_menu', '1')
->addFieldToFilter('parent_id', array('eq' => $parentId))
->setOrder('position','asc');
}
// TODO check for $parentID != $rootID is a little hacky, need to DRY this up
$html .= ($parentId != $rootID) ? '<ul>' : null;
// Loop over categories at the current level
foreach($children as $child) {
$childId = $child->getId();
$parent = (count($child->getChildren()) > 0) ? $child->getChildren() : false;
$classes = [];
// Build out class lists
if($parent) {
$classes[] = 'parent';
}
if(in_array($childId, $parentIDs, true) || count($children) == 1) {
$classes[] = "current active";
}
if($childId == $currentID) {
$classes[] = "current-page";
}
// Build out the list item with the values appropriate to the type
if($type == 'category') {
$html .= checkChildHtml($parentId, '<li class="' . implode(' ', $classes) . '">' . ($parent ? '<button class="toggle"></button>' : null) . '' . $child->getName() . '');
}
else {
$html .= checkChildHtml($parentId, '<li class="' . implode(' ', $classes) . '">' . ($parent ? '<button class="toggle"></button>' : null) . '' . $child->title . '');
}
// Append the list html (if not root page)
if($parent) {
// Get the categories below this page
$html .= buildChildMenu($type, $currentID, $child->getId(), true, $parentIDs, $rootID);
}
// Close the list (if not root product page)
$html .= checkChildHtml($parentId, '</li>');
}
$html .= checkChildHtml($parentId, '</ul>');
return $html;
}
// Build out menu from root level down
$categoryListHtml = buildChildMenu($type, $currentID, $rootID, false, $parentIDs, $rootID);
Are there any obvious bottlenecks here? If not, what is best practise in this scenario?
For instance, should I AJAX the children when requested? Or maybe cache the menu? Or... something else?
Okay, the issue was that I had my cache turned off whilst I was developing this menu. With the cache enabled the processing time is insignificant.
Related
I've faced funny bug when modifying the e-store engine:
// ...
$this->toolbar_title[] = 'Products';
// ...
print_r($this->toolbar_title);
/*
Array (
[0] => Products
)
*/
$this->toolbar_title[] = 'Filtered by: name';
print_r($this->toolbar_title);
/*
Array
(
[0] => Products
[2] => Filtered by: name
)
*/
// ...
wat??? where is the "1" index??
Tried to reproduce this in clean stand-alone php script - nope! the added element has index "1" as expected. but when doing the same within the engine code - new element has index "2".
There are no setters, no "[]" overloading found, even no any access to the $this->toolbar_title elements by index, only pushing via [];
What the magic is this? What and where should I seek to find the reason?
PHP 5.6, PrestaShop 1.6 engine.
Thanks a lot in advance for any clue.
UPD: the exact code fragment from engine
if ($filter = $this->addFiltersToBreadcrumbs()) {
echo'131-';print_r($this->toolbar_title);
$this->toolbar_title[] = $filter;
echo'132-';var_dump($this->toolbar_title);
}
where addFiltersToBreadcrumbs returns the string and make NO any access to toolbar_title
UPD2:
public function addFiltersToBreadcrumbs()
{
if ($this->filter && is_array($this->fields_list)) {
$filters = array();
foreach ($this->fields_list as $field => $t) {
if (isset($t['filter_key'])) {
$field = $t['filter_key'];
}
if (($val = Tools::getValue($this->table.'Filter_'.$field)) || $val = $this->context->cookie->{$this->getCookieFilterPrefix().$this->table.'Filter_'.$field}) {
if (!is_array($val)) {
$filter_value = '';
if (isset($t['type']) && $t['type'] == 'bool') {
$filter_value = ((bool)$val) ? $this->l('yes') : $this->l('no');
} elseif (isset($t['type']) && $t['type'] == 'date' || isset($t['type']) && $t['type'] == 'datetime') {
$date = Tools::unSerialize($val);
if (isset($date[0])) {
$filter_value = $date[0];
if (isset($date[1]) && !empty($date[1])) {
$filter_value .= ' - '.$date[1];
}
}
} elseif (is_string($val)) {
$filter_value = htmlspecialchars($val, ENT_QUOTES, 'UTF-8');
}
if (!empty($filter_value)) {
$filters[] = sprintf($this->l('%s: %s'), $t['title'], $filter_value);
}
} else {
$filter_value = '';
foreach ($val as $v) {
if (is_string($v) && !empty($v)) {
$filter_value .= ' - '.htmlspecialchars($v, ENT_QUOTES, 'UTF-8');
}
}
$filter_value = ltrim($filter_value, ' -');
if (!empty($filter_value)) {
$filters[] = sprintf($this->l('%s: %s'), $t['title'], $filter_value);
}
}
}
}
if (count($filters)) {
return sprintf($this->l('filter by %s'), implode(', ', $filters));
}
}
}
For php, if you unset one index in array, the index will appear discontinuous growth.
$this->toolbar_title = array_values($this->toolbar_title);
will rebuild the index.
I want to make a sidebar menu like the website mega.nz in php.. like this
I have tried with this code:
function foldersList($folderName = NULL) {
$return = '';
$globFolder = ($folderName != NULL ? $folderName : './server/'.$_SESSION['username']) . "/*";
foreach (glob($globFolder, GLOB_ONLYDIR) as $subFolder) {
$baseFolder = basename($subFolder) . '';
// okay let me see
// call function to check subfolders - don't forget write `/`
$subFolders = foldersList($subFolder);
$return .= '<li><a><i class="fa fa-desktop"></i>' . $baseFolder . '<span class="fa fa-chevron-down"></span></a>';
$return .= '<ul class="nav child_menu">';
// if subfolder exist add to return variable
$return .= $subFolders != '' ? $subFolders : '';
$return .= '</ul>';
$return .= '</li>';
}
return $return;
}
but the problem is the folder doesn't recognize if inside the folder, have one more folder or have a list of files.
I want, if folder have list of files inside, go to another link
if folder, have one more folder, makes dropdown menu, to show this folder.
To skip empty folders or folders that have only files change the function to:
function foldersList($folderName = NULL) {
$return = '';
$globFolder = ($folderName != NULL ? $folderName : './server/'.$_SESSION['username']) . "/*";
$folders = glob($globFolder, GLOB_ONLYDIR);
if(!empty($folders)) {
foreach ($folders as $subFolder) {
$baseFolder = basename($subFolder) . '';
// okay let me see
// call function to check subfolders - don't forget write `/`
$subFolders = foldersList($subFolder);
$return .= '<li><a><i class="fa fa-desktop"></i>' . $baseFolder . '<span class="fa fa-chevron-down"></span></a>';
$return .= '<ul class="nav child_menu">';
// if subfolder exist add to return variable
$return .= $subFolders != '' ? $subFolders : '';
$return .= '</ul>';
$return .= '</li>';
}
}
return $return;}
Other solution: use a nice plug-in to help you with the tree https://www.jstree.com/
EDIT: added example usage of jstree
Change your function to:
function foldersList($folderName = NULL) {
$return = '';
$globFolder = ($folderName != NULL ? $folderName :
'./server/'.$_SESSION['username']) . "/*";
$folders = glob($globFolder, GLOB_ONLYDIR);
$foldersArray = [];
if(!empty($folders)) {
foreach ($folders as $subFolder) {
$baseFolder = basename($subFolder) . '';
// okay let me see
// call function to check subfolders - don't forget write `/`
$subFolders = foldersList($subFolder);
$foldersArray[] = [
'text' => $baseFolder,
'children' => $subFolders
];
}
}
return $foldersArray;}
Here is a demo with the output of modified function: https://jsfiddle.net/mrazvan/3sxh0b3c/1/
Create a file where you run the function and echo the json output in one php file:
echo json_encode( foldersList('path/to/folder/'));exit;
To fetch the output from back end, you can try using the following JS code:
$(function() {
$.ajax({
type: "GET",
dataType: "json",
url: "path/to/phpfile.php",
success: function(data) {
$('#tree-container').jstree({
'plugins' : ['types'],
'core' : {
'data' : data,
'themes' : {
'variant' : 'medium'
}
}
});
}
});
});
I have to extend an existing menu to a multilevel one. I am having a hard time wrapping my head around it so I'm hoping somebody can help me out.
First I've added another table in the database with the name parent_id.
Then I'd like to see if this column is filled out, so greater than > 0.
And then of course, check to see if id == parent_id.
If so, I'd like to display my submenu on hover of the parent item.
My current menu is a multi lang menu.
This is my current model:
var $default_order_by = array('position');
function findView($page)
{
$language = $this->config->item('language');
$p = new Page();
$p->where('url_' . $language, $page)->get();
return $p->view;
}
function findPageMenu($page)
{
$language = $this->config->item('language');
$p = new Page();
$p->where('url_' . $language, $page)->get();
return $p->menu;
}
function findAllByView()
{
$pages = new Page();
$result = array();
foreach ($pages->get() as $page)
$result[$page->view] = $page;
return $result;
}
function getMenu()
{
$pages = new Page();
if ($this->session->userdata('is_admin'))
return $pages->where('position >', 0)->get();
else
return $pages->where('position >', 0)->where('admin', 0)->get();
}
function getUrlByView($view)
{
$page = new Page();
$page->where('view', $view)->get();
$language = $this->config->item('language');
return $page->{'url_' . $language};
}
And this is my view:
<ul class="primary-nav">
<?php foreach($menu as $page): ?>
<li class="primary-nav__item">
<a class="primary-nav__link" <?php if ($page_menu == $page->view): ?>class="active" <?php endif; ?>href="<?php echo base_url() . $this->config->item('language_abbr') . '/' . $page->{'url_' . $this->config->item('language')}; ?>">
<?php echo mb_strtoupper($page->{'title_' . $this->config->item('language')}, 'UTF-8'); ?>
</a>
</li>
<?php endforeach; ?>
I was thinking of doing something like this:
function getSubMenu()
{
if ($this->session->userdata('is_admin'))
return $pages->where('position >', 0 && 'parent_id >', 0)->get();
else
// return $pages->where('position >', 0 && 'parent_id >', 0)->get();
echo '<h1> yay </h1>';
}
(ignore the yay, lol) But this obviously doesn't even begin to cut it.
Suggestion: You can add the "parent_id" in the same table. If there is a parent then fill it with parent id or with 0.
Answer: Get your data as a one dimentional array with all the rows with parent id. Then use the below function to create a multi dimensional array with parent and child.
function formatTree($tree, $parent = NULL) {
$treeArray = array();
foreach ($tree as $item) {
if ($item['menu_parent'] == 0) {
$treeArray[$item['menu_id']] = $item;
}
else {
$treeArray[$item['menu_parent']]['sub'][] = $item;
}
}
return $treeArray;
}
Now use the below function to make an intended ul-li list of parent-child menu
function buildMenu($menu_array, $is_sub = FALSE) {
$attr = (!$is_sub) ? ' class="sidebar-menu"' : ' class="treeview-menu"';
$menu = "<ul>"; // Open the menu container
foreach ($menu_array as $id => $properties) {
if (!isset($properties['sub'])) {
$is_sub = TRUE;
}
elseif (empty($properties['sub'])) {
$is_sub = TRUE;
}
foreach ($properties as $key => $val) {
if (is_array($val) && !empty($val)) {
$sub = $this -> buildMenu($val, TRUE);
}
else {
$sub = NULL;
$$key = $val;
}
}
if ($properties['menu_url']) {
$url = $properties['menu_url'];
}
$menu .= "<li>" . $menu_name . "</li>";
unset($url, $menu_name, $sub);
}
return $menu . "</ul>";
}
I am using this in my application. The array structure of menu data should be
array(
[0] => array(
'menu_name' => 'name',
'menu_url' => 'url',
'menu_id' => 'id',
'menu_parent' => 'parent id'
),
[1] => array(
'menu_name' => 'name',
'menu_url' => 'url',
'menu_id' => 'id',
'menu_parent' => 'parent id'
)
)
The following code will set the class nav on the first level UL
$mainNav = public_nav_main();
$mainNav->setUlClass('nav')->setUlId('main-menu-left');
However im using bootstrap and so want the second level ul to have the class 'dropdown-menu'
I cant seem to find a reference to get this sorted.
Zend is being used as the base structure in the software im using, Omeka. Unfortunately Omeka doesnt have a way to do this natively so I am having to dive into the underlying Zend FW although I dont want to modify that too much as it might be changed.
You might just want to write a totally new View Helper based off Zend_View_Helper_Navigation_HelperAbstract.
Looking on GitHub for a bootstrap compatible helper based on that abstract I did encounter this: https://github.com/michaelmoussa/zf1-navigation-view-helper-bootstrap/blob/master/library/ZFBootstrap/View/Helper/Navigation/Menu.php which takes an interesting approach, post processing the markup generated from the out-of-the-box helpers.
I took a slightly different approach recently and just hacked the heck out of Zend_View_Helper_Navigation_Menu. Here is a unified diff summarizing those changes: http://pastebin.com/mrJG8QCt Better though to extend the class...
I didn't deal with sub-menus, however the issues I ran into were...
Way to add aria-role to <li> elements.
Avoid collision when rendering the menu twice - two representations - collapsed bootstrap style and traditional one for larger viewports. -Maybe ZF already offered something to work around this? Didn't jump out at me if there was.
This code shows the methods you need to tweak:
class MyMenu extends Zend_View_Helper_Navigation_Menu
{
/**
* Want a way to set aria role on menu li elements because its 2015 yo
*
* #var string
*/
protected $_liRole = '';
/**
* Workaround so I can render the damn thing twice on the same page and not collide IDs on the <a>'s
* Issue arose when adopting bootstrap and rendering both full page nav and collapsed nav bar
*
* #var string
*/
protected $_idAlias = '';
public function setLiRole($liRole)
{
if (is_string($liRole)) {
$this->_liRole = $liRole;
}
return $this;
}
public function getLiRole()
{
return $this->_liRole;
}
public function setIdAlias($alias)
{
$this->_idAlias = $alias;
return $this;
}
public function getIdAlias()
{
return $this->_idAlias;
}
public function renderMenu(Zend_Navigation_Container $container = null, array $options = array())
{
$this->setLiRole($options['liRole']);
$this->setIdAlias($options['idAlias']);
return parent::renderMenu($container, $options);
}
/**
* Returns an HTML string containing an 'a' element for the given page if
* the page's href is not empty, and a 'span' element if it is empty
*
* Overrides {#link Zend_View_Helper_Navigation_Abstract::htmlify()}.
*
* #param Zend_Navigation_Page $page page to generate HTML for
* #return string HTML string for the given page
*/
public function htmlify(Zend_Navigation_Page $page)
{
// get label and title for translating
$label = $page->getLabel();
$title = $page->getTitle();
// translate label and title?
if ($this->getUseTranslator() && $t = $this->getTranslator()) {
if (is_string($label) && !empty($label)) {
$label = $t->translate($label);
}
if (is_string($title) && !empty($title)) {
$title = $t->translate($title);
}
}
// get attribs for element
$attribs = array(
'id' => $this->getIdAlias() . $page->getId(),
'title' => $title,
'class' => $page->getClass()
);
// does page have a href?
if ($href = $page->getHref()) {
$element = 'a';
$attribs['href'] = $href;
$attribs['target'] = $page->getTarget();
} else {
$element = 'span';
}
return '<' . $element . $this->_htmlAttribs($attribs) . '><span class="span-nav-icon"></span><span>'
. str_replace(chr(32), ' ', $this->view->escape($label))
. '</span></' . $element . '>';
}
/**
* Normalizes given render options
*
* #param array $options [optional] options to normalize
* #return array normalized options
*/
protected function _normalizeOptions(array $options = array())
{
if (isset($options['indent'])) {
$options['indent'] = $this->_getWhitespace($options['indent']);
} else {
$options['indent'] = $this->getIndent();
}
if (isset($options['ulClass']) && $options['ulClass'] !== null) {
$options['ulClass'] = (string) $options['ulClass'];
} else {
$options['ulClass'] = $this->getUlClass();
}
if (isset($options['liRole']) && $options['liRole'] !== null) {
$options['liRole'] = (string) $options['liRole'];
} else {
$options['liRole'] = $this->getLiRole();
}
if (isset($options['idAlias']) && $options['idAlias'] !== null) {
$options['idAlias'] = (string) $options['idAlias'];
} else {
$options['idAlias'] = '';
}
if (array_key_exists('minDepth', $options)) {
if (null !== $options['minDepth']) {
$options['minDepth'] = (int) $options['minDepth'];
}
} else {
$options['minDepth'] = $this->getMinDepth();
}
if ($options['minDepth'] < 0 || $options['minDepth'] === null) {
$options['minDepth'] = 0;
}
if (array_key_exists('maxDepth', $options)) {
if (null !== $options['maxDepth']) {
$options['maxDepth'] = (int) $options['maxDepth'];
}
} else {
$options['maxDepth'] = $this->getMaxDepth();
}
if (!isset($options['onlyActiveBranch'])) {
$options['onlyActiveBranch'] = $this->getOnlyActiveBranch();
}
if (!isset($options['renderParents'])) {
$options['renderParents'] = $this->getRenderParents();
}
return $options;
}
/**
* Renders the deepest active menu within [$minDepth, $maxDeth], (called
* from {#link renderMenu()})
*
* #param Zend_Navigation_Container $container container to render
* #param array $active active page and depth
* #param string $ulClass CSS class for first UL
* #param string $indent initial indentation
* #param int|null $minDepth minimum depth
* #param int|null $maxDepth maximum depth
* #return string rendered menu
*/
protected function _renderDeepestMenu(Zend_Navigation_Container $container,
$ulClass,
$indent,
$minDepth,
$maxDepth)
{
if (!$active = $this->findActive($container, $minDepth - 1, $maxDepth)) {
return '';
}
// special case if active page is one below minDepth
if ($active['depth'] < $minDepth) {
if (!$active['page']->hasPages()) {
return '';
}
} else if (!$active['page']->hasPages()) {
// found pages has no children; render siblings
$active['page'] = $active['page']->getParent();
} else if (is_int($maxDepth) && $active['depth'] +1 > $maxDepth) {
// children are below max depth; render siblings
$active['page'] = $active['page']->getParent();
}
$ulClass = $ulClass ? ' class="' . $ulClass . '"' : '';
$html = $indent . '<ul' . $ulClass . '>' . self::EOL;
$liRole = (! empty($this->getLiRole())) ? "role=\"{$this->getLiRole()}\"" : "";
foreach ($active['page'] as $subPage) {
if (!$this->accept($subPage)) {
continue;
}
$liClass = $subPage->isActive(true) ? ' class="active"' : '';
$html .= $indent . ' <li' . $liClass . ' ' . $liRole . '>' . self::EOL;
$html .= $indent . ' ' . $this->htmlify($subPage) . self::EOL;
$html .= $indent . ' </li>' . self::EOL;
}
$html .= $indent . '</ul>';
return $html;
}
/**
* Renders a normal menu (called from {#link renderMenu()})
*
* #param Zend_Navigation_Container $container container to render
* #param string $ulClass CSS class for first UL
* #param string $indent initial indentation
* #param int|null $minDepth minimum depth
* #param int|null $maxDepth maximum depth
* #param bool $onlyActive render only active branch?
* #return string
*/
protected function _renderMenu(Zend_Navigation_Container $container,
$ulClass,
$indent,
$minDepth,
$maxDepth,
$onlyActive)
{
$html = '';
// find deepest active
if ($found = $this->findActive($container, $minDepth, $maxDepth)) {
$foundPage = $found['page'];
$foundDepth = $found['depth'];
} else {
$foundPage = null;
}
// create iterator
$iterator = new RecursiveIteratorIterator($container,
RecursiveIteratorIterator::SELF_FIRST);
if (is_int($maxDepth)) {
$iterator->setMaxDepth($maxDepth);
}
// iterate container
$prevDepth = -1;
foreach ($iterator as $page) {
$depth = $iterator->getDepth();
$isActive = $page->isActive(true);
if ($depth < $minDepth || !$this->accept($page)) {
// page is below minDepth or not accepted by acl/visibilty
continue;
} else if ($onlyActive && !$isActive) {
// page is not active itself, but might be in the active branch
$accept = false;
if ($foundPage) {
if ($foundPage->hasPage($page)) {
// accept if page is a direct child of the active page
$accept = true;
} else if ($foundPage->getParent()->hasPage($page)) {
// page is a sibling of the active page...
if (!$foundPage->hasPages() ||
is_int($maxDepth) && $foundDepth + 1 > $maxDepth) {
// accept if active page has no children, or the
// children are too deep to be rendered
$accept = true;
}
}
}
if (!$accept) {
continue;
}
}
$liRole = (! empty($this->getLiRole())) ? "role=\"{$this->getLiRole()}\"" : "";
// make sure indentation is correct
$depth -= $minDepth;
$myIndent = $indent . str_repeat(' ', $depth);
if ($depth > $prevDepth) {
// start new ul tag
if ($ulClass && $depth == 0) {
$ulClass = ' class="' . $ulClass . '"';
} else {
$ulClass = '';
}
$html .= $myIndent . '<ul' . $ulClass . '>' . self::EOL;
} else if ($prevDepth > $depth) {
// close li/ul tags until we're at current depth
for ($i = $prevDepth; $i > $depth; $i--) {
$ind = $indent . str_repeat(' ', $i);
$html .= $ind . ' </li>' . self::EOL;
$html .= $ind . '</ul>' . self::EOL;
}
// close previous li tag
$html .= $myIndent . ' </li>' . self::EOL;
} else {
// close previous li tag
$html .= $myIndent . ' </li>' . self::EOL;
}
// render li tag and page
$liClass = $isActive ? ' class="active"' : '';
$html .= $myIndent . ' <li' . $liClass . ' ' . $liRole . '>' . self::EOL
. $myIndent . ' ' . $this->htmlify($page) . self::EOL;
// store as previous depth for next iteration
$prevDepth = $depth;
}
if ($html) {
// done iterating container; close open ul/li tags
for ($i = $prevDepth+1; $i > 0; $i--) {
$myIndent = $indent . str_repeat(' ', $i-1);
$html .= $myIndent . ' </li>' . self::EOL
. $myIndent . '</ul>' . self::EOL;
}
$html = rtrim($html, self::EOL);
}
return $html;
}
}
Admittedly a lot of code. Might be good to diff the class you have now against this http://pastebin.com/qiD2ULsz - and then seeing what the touch points are, creating a new extending class. Really just the new properties and some "tweaks" to the string concatenation it does to render the markup.
I don't specifically address class on "second level ul's" but passing in an additional property would be trivial and follow the same changes I did make.
Hope this helps some. ZF 1.x shows its age a bit and these view helpers were never that great. The underlying Nav code isn't too bad, so again, maybe just start from scratch and write your own code to render a Zend Nav Container. Good luck.
This is admittedly an ugly hack, but you could do it by processing the output of public_nav_main() with a regular expression. So in the header.php file you would replace:
echo public_nav_main();
with
echo preg_replace( "/(?<!\/)ul(?!.*?nav)/", 'ul class="dropdown-menu"', public_nav_main() );
This will only work if you only have 2 levels in your menu, since the above regular expression will also add the class="dropdown-menu" to all ul elements below the top level ul.
Benefits
Simple
Achieves what you want to do
Doesn't require writing helpers or modifying the underlying framework
Should not break when the underlying framework is updated unless the update renders something other than a string from public_nav_main()
Downsides
Won't work if your menu has more than two levels
Will result in having two class attributes in your 2nd and lower level ul elements in the event that they already have a class attribute
Here is the scenario, and let me start off saying any help would be a god-send, I have cloned the Article Category module inside Joomla! Basically changed all the articles_category to breed_articles (for my purpose). This worked fine, where I am stuck at is where in the module code can I define a specific category to pull articles from.
I know I can do this in the backend but I am working on a dynamic way to pull articles by categories and need to define the category in the module code. That being said, I will also need to pull the categoies by the slug not id.
Where I have been looking is the helper.php file in the module and I believe that I am on the right path there. I have tried replacing a few things and tracing the code but I am not very familiar with Joomla!
helper.php
$com_path = JPATH_SITE.'/components/com_content/';
require_once $com_path.'router.php';
require_once $com_path.'helpers/route.php';
JModelLegacy::addIncludePath($com_path . '/models', 'ContentModel');
abstract class modBreedArticlesHelper
{
public static function getList(&$params)
{
// Get an instance of the generic articles model
$articles = JModelLegacy::getInstance('Articles', 'ContentModel', array('ignore_request' => true));
// Set application parameters in model
$app = JFactory::getApplication();
$appParams = $app->getParams();
$articles->setState('params', $appParams);
// Set the filters based on the module params
$articles->setState('list.start', 0);
$articles->setState('list.limit', (int) $params->get('count', 0));
$articles->setState('filter.published', 1);
// Access filter
$access = !JComponentHelper::getParams('com_content')->get('show_noauth');
$authorised = JAccess::getAuthorisedViewLevels(JFactory::getUser()->get('id'));
$articles->setState('filter.access', $access);
// Prep for Normal or Dynamic Modes
$mode = $params->get('mode', 'normal');
switch ($mode)
{
case 'dynamic':
$option = $app->input->get('option');
$view = $app->input->get('view');
if ($option === 'com_content') {
switch($view)
{
case 'category':
$catids = array($app->input->getInt('id'));
break;
case 'categories':
$catids = array($app->input->getInt('id'));
break;
case 'article':
if ($params->get('show_on_article_page', 1)) {
$article_id = $app->input->getInt('id');
$catid = $app->input->getInt('catid');
if (!$catid) {
// Get an instance of the generic article model
$article = JModelLegacy::getInstance('Article', 'ContentModel', array('ignore_request' => true));
$article->setState('params', $appParams);
$article->setState('filter.published', 1);
$article->setState('article.id', (int) $article_id);
$item = $article->getItem();
$catids = array($item->catid);
}
else {
$catids = array($catid);
}
}
else {
// Return right away if show_on_article_page option is off
return;
}
break;
case 'featured':
default:
// Return right away if not on the category or article views
return;
}
}
else {
// Return right away if not on a com_content page
return;
}
break;
case 'normal':
default:
$catids = $params->get('catid');
$articles->setState('filter.category_id.include', (bool) $params->get('category_filtering_type', 1));
break;
}
// Category filter
if ($catids) {
if ($params->get('show_child_category_articles', 0) && (int) $params->get('levels', 0) > 0) {
// Get an instance of the generic categories model
$categories = JModelLegacy::getInstance('Categories', 'ContentModel', array('ignore_request' => true));
$categories->setState('params', $appParams);
$levels = $params->get('levels', 1) ? $params->get('levels', 1) : 9999;
$categories->setState('filter.get_children', $levels);
$categories->setState('filter.published', 1);
$categories->setState('filter.access', $access);
$additional_catids = array();
foreach($catids as $catid)
{
$categories->setState('filter.parentId', $catid);
$recursive = true;
$items = $categories->getItems($recursive);
if ($items)
{
foreach($items as $category)
{
$condition = (($category->level - $categories->getParent()->level) <= $levels);
if ($condition) {
$additional_catids[] = $category->id;
}
}
}
}
$catids = array_unique(array_merge($catids, $additional_catids));
}
$articles->setState('filter.category_id', $catids);
}
// Ordering
$articles->setState('list.ordering', $params->get('article_ordering', 'a.ordering'));
$articles->setState('list.direction', $params->get('article_ordering_direction', 'ASC'));
// New Parameters
$articles->setState('filter.featured', $params->get('show_front', 'show'));
$articles->setState('filter.author_id', $params->get('created_by', ""));
$articles->setState('filter.author_id.include', $params->get('author_filtering_type', 1));
$articles->setState('filter.author_alias', $params->get('created_by_alias', ""));
$articles->setState('filter.author_alias.include', $params->get('author_alias_filtering_type', 1));
$excluded_articles = $params->get('excluded_articles', '');
if ($excluded_articles) {
$excluded_articles = explode("\r\n", $excluded_articles);
$articles->setState('filter.article_id', $excluded_articles);
$articles->setState('filter.article_id.include', false); // Exclude
}
$date_filtering = $params->get('date_filtering', 'off');
if ($date_filtering !== 'off') {
$articles->setState('filter.date_filtering', $date_filtering);
$articles->setState('filter.date_field', $params->get('date_field', 'a.created'));
$articles->setState('filter.start_date_range', $params->get('start_date_range', '1000-01-01 00:00:00'));
$articles->setState('filter.end_date_range', $params->get('end_date_range', '9999-12-31 23:59:59'));
$articles->setState('filter.relative_date', $params->get('relative_date', 30));
}
// Filter by language
$articles->setState('filter.language', $app->getLanguageFilter());
$items = $articles->getItems();
// Display options
$show_date = $params->get('show_date', 0);
$show_date_field = $params->get('show_date_field', 'created');
$show_date_format = $params->get('show_date_format', 'Y-m-d H:i:s');
$show_category = $params->get('show_category', 0);
$show_hits = $params->get('show_hits', 0);
$show_author = $params->get('show_author', 0);
$show_introtext = $params->get('show_introtext', 0);
$introtext_limit = $params->get('introtext_limit', 100);
// Find current Article ID if on an article page
$option = $app->input->get('option');
$view = $app->input->get('view');
if ($option === 'com_content' && $view === 'article') {
$active_article_id = $app->input->getInt('id');
}
else {
$active_article_id = 0;
}
// Prepare data for display using display options
foreach ($items as &$item)
{
$item->slug = $item->id.':'.$item->alias;
$item->catslug = $item->catid ? $item->catid .':'.$item->category_alias : $item->catid;
if ($access || in_array($item->access, $authorised))
{
// We know that user has the privilege to view the article
$item->link = JRoute::_(ContentHelperRoute::getArticleRoute($item->slug, $item->catslug));
}
else
{
$app = JFactory::getApplication();
$menu = $app->getMenu();
$menuitems = $menu->getItems('link', 'index.php?option=com_users&view=login');
if (isset($menuitems[0]))
{
$Itemid = $menuitems[0]->id;
}
elseif ($app->input->getInt('Itemid') > 0)
{
// Use Itemid from requesting page only if there is no existing menu
$Itemid = $app->input->getInt('Itemid');
}
$item->link = JRoute::_('index.php?option=com_users&view=login&Itemid='.$Itemid);
}
// Used for styling the active article
$item->active = $item->id == $active_article_id ? 'active' : '';
$item->displayDate = '';
if ($show_date) {
$item->displayDate = JHTML::_('date', $item->$show_date_field, $show_date_format);
}
if ($item->catid) {
$item->displayCategoryLink = JRoute::_(ContentHelperRoute::getCategoryRoute($item->catid));
$item->displayCategoryTitle = $show_category ? ''.$item->category_title.'' : '';
}
else {
$item->displayCategoryTitle = $show_category ? $item->category_title : '';
}
$item->displayHits = $show_hits ? $item->hits : '';
$item->displayAuthorName = $show_author ? $item->author : '';
if ($show_introtext) {
$item->introtext = JHtml::_('content.prepare', $item->introtext, '', 'mod_articles_category.content');
$item->introtext = self::_cleanIntrotext($item->introtext);
}
$item->displayIntrotext = $show_introtext ? self::truncate($item->introtext, $introtext_limit) : '';
$item->displayReadmore = $item->alternative_readmore;
}
return $items;
}
public static function _cleanIntrotext($introtext)
{
$introtext = str_replace('<p>', ' ', $introtext);
$introtext = str_replace('</p>', ' ', $introtext);
$introtext = strip_tags($introtext, '<a><em><strong>');
$introtext = trim($introtext);
return $introtext;
}
/**
* Method to truncate introtext
*
* The goal is to get the proper length plain text string with as much of
* the html intact as possible with all tags properly closed.
*
* #param string $html The content of the introtext to be truncated
* #param integer $maxLength The maximum number of charactes to render
*
* #return string The truncated string
*/
public static function truncate($html, $maxLength = 0)
{
$baseLength = strlen($html);
$diffLength = 0;
// First get the plain text string. This is the rendered text we want to end up with.
$ptString = JHtml::_('string.truncate', $html, $maxLength, $noSplit = true, $allowHtml = false);
for ($maxLength; $maxLength < $baseLength;)
{
// Now get the string if we allow html.
$htmlString = JHtml::_('string.truncate', $html, $maxLength, $noSplit = true, $allowHtml = true);
// Now get the plain text from the html string.
$htmlStringToPtString = JHtml::_('string.truncate', $htmlString, $maxLength, $noSplit = true, $allowHtml = false);
// If the new plain text string matches the original plain text string we are done.
if ($ptString == $htmlStringToPtString)
{
return $htmlString;
}
// Get the number of html tag characters in the first $maxlength characters
$diffLength = strlen($ptString) - strlen($htmlStringToPtString);
// Set new $maxlength that adjusts for the html tags
$maxLength += $diffLength;
if ($baseLength <= $maxLength || $diffLength <= 0)
{
return $htmlString;
}
}
return $html;
}
public static function groupBy($list, $fieldName, $article_grouping_direction, $fieldNameToKeep = null)
{
$grouped = array();
if (!is_array($list)) {
if ($list == '') {
return $grouped;
}
$list = array($list);
}
foreach($list as $key => $item)
{
if (!isset($grouped[$item->$fieldName])) {
$grouped[$item->$fieldName] = array();
}
if (is_null($fieldNameToKeep)) {
$grouped[$item->$fieldName][$key] = $item;
}
else {
$grouped[$item->$fieldName][$key] = $item->$fieldNameToKeep;
}
unset($list[$key]);
}
$article_grouping_direction($grouped);
return $grouped;
}
public static function groupByDate($list, $type = 'year', $article_grouping_direction, $month_year_format = 'F Y')
{
$grouped = array();
if (!is_array($list)) {
if ($list == '') {
return $grouped;
}
$list = array($list);
}
foreach($list as $key => $item)
{
switch($type)
{
case 'month_year':
$month_year = JString::substr($item->created, 0, 7);
if (!isset($grouped[$month_year])) {
$grouped[$month_year] = array();
}
$grouped[$month_year][$key] = $item;
break;
case 'year':
default:
$year = JString::substr($item->created, 0, 4);
if (!isset($grouped[$year])) {
$grouped[$year] = array();
}
$grouped[$year][$key] = $item;
break;
}
unset($list[$key]);
}
$article_grouping_direction($grouped);
if ($type === 'month_year') {
foreach($grouped as $group => $items)
{
$date = new JDate($group);
$formatted_group = $date->format($month_year_format);
$grouped[$formatted_group] = $items;
unset($grouped[$group]);
}
}
return $grouped;
}
}
It looks like my issue was really specific to the project I am working on. If anyone happens to find this or wants to accomplish something similar, read on.
Decided to accomplish what I was doing using a JDatabase query. To retrieve the articles using the category alias, I queried the DB to match the alias I was giving the query to the category ID. Then joined the content table to get the article information.
All the information you need to make a proper JDatabase query can be found here: http://docs.joomla.org/Accessing_the_database_using_JDatabase