/ */ class Category_Module extends P4Cms_Module_Integration { /** * Subscribe to the relevant topics. */ public static function init() { // contribute the list of categories as a dynamic menu item. P4Cms_PubSub::subscribe('p4cms.navigation.dynamicHandlers', function() { $handler = new P4Cms_Navigation_DynamicHandler; $handler->setId('categories') ->setLabel('Category Listing') ->setExpansionCallback('Category_Module::expandDynamicMenu') ->setFormCallback( function(Zend_Form $form) { // add a field to control if content entries in categories // are included when dynamic category menu items are expanded. $form->addElement( 'checkbox', 'includeEntries', array( 'label' => 'Include Content', 'description' => "Include content entries in categories.", 'checked' => false ) ); return $form; } ); return array($handler); } ); // participate in content editing by providing a subform. P4Cms_PubSub::subscribe('p4cms.content.form.subForms', function(Content_Form_Content $form) { // enforce permissions. $user = P4Cms_User::fetchActive(); if (!$user->isAllowed('categories', 'associate')) { return; } return new Category_Form_Content( array( 'name' => 'category', 'idPrefix' => $form->getIdPrefix(), 'order' => -30 ) ); } ); // participate in content editing by populating categories sub-form. P4Cms_PubSub::subscribe('p4cms.content.form.populate', function(Content_Form_Content $form, array $values) { // nothing to do if no category sub-form. if (!$form->getSubForm('category')) { return; } // pull categories from entry if not in values array. $categories = array(); if (isset($values['category']['categories'])) { $categories = $values['category']['categories']; } else { $id = $form->getEntry()->getId(); if ($id !== null) { $categories = Category_Model_Category::fetchIdsByEntry($id); } } $form->getSubForm('category') ->getElement('categories') ->setValue($categories); } ); // participate in content editing - don't store categories on content // directly, store category associations in category records instead. P4Cms_PubSub::subscribe('p4cms.content.record.preSave', function(P4Cms_Content $entry) { // move category out of fields list and into a public // property so it doesn't get saved with the record if ($entry->hasField('category')) { $category = $entry->getValue('category'); $entry->unsetValue('category'); $entry->category = $category; } } ); P4Cms_PubSub::subscribe('p4cms.content.record.postSave', function(P4Cms_Content $entry) { // if no category data present, no category changes required. if (!isset($entry->category)) { return; } // pull off the latest category associations. $categories = isset($entry->category['categories']) ? $entry->category['categories'] : array(); Category_Model_Category::setEntryCategories($entry->getId(), $categories); } ); // provide form to filter content by category. P4Cms_PubSub::subscribe('p4cms.content.grid.form.subForms', function(Zend_Form $form) { // enforce permissions. $user = P4Cms_User::fetchActive(); if (!$user->isAllowed('categories', 'access')) { return; } $form = new Category_Form_Content; $form->setName('category') ->setOrder(15) ->setLegend(null) ->removeElement('addCategory'); $categories = $form->getElement('categories'); $categories->setLabel('Category') ->setAttrib('autoApply', true); // if there are no categories, don't return form. if (!count($categories->getMultiOptions())) { return null; } return $form; } ); // filter content list by category. P4Cms_PubSub::subscribe('p4cms.content.grid.populate', function(P4Cms_Record_Query $query, Zend_Form $form) { $values = $form->getValues(); // extract selected categories. $categories = isset($values['category']['categories']) ? $values['category']['categories'] : null; // early exit if no categories selected. if (!is_array($categories) || !count($categories)) { return; } // check for existing 'categories' options and intersect if found $filter = $query->getFilter() ?: new P4Cms_Record_Filter; if (is_array($filter->getOption('categories'))) { $categories = array_intersect($categories, $filter->getOption('categories')); } // The category module subscribes to the p4cms.content.record.query topic // so we can just add our categories to the query here and they will be // filtered when the query is executed. $filter->setOption('categories', $categories); $query->setFilter($filter); } ); // provide category tree actions P4Cms_PubSub::subscribe('p4cms.category.grid.actions', function($actions) { $actions->addPages( array( array( 'label' => 'View', 'onClick' => 'p4cms.category.grid.Actions.onClickView();', 'order' => '10' ), array( 'label' => 'Edit', 'onClick' => 'p4cms.category.grid.Actions.onClickEdit();', 'order' => '20' ), array( 'label' => 'Delete', 'onClick' => 'p4cms.category.grid.Actions.onClickDelete();', 'order' => '30' ) ) ); } ); // provide form to search categories P4Cms_PubSub::subscribe('p4cms.category.grid.form.subForms', function(Zend_Form $form) { return new Ui_Form_GridSearch; } ); // filter categories by keyword search P4Cms_PubSub::subscribe('p4cms.category.grid.populate', function(P4Cms_Model_Iterator $categories, Zend_Form $form) { $values = $form->getValues(); // extract search query. $query = isset($values['search']['query']) ? $values['search']['query'] : null; // early exit if no query. if (!$query) { return null; } // filter categories by the search query $categories->search('title', $query); } ); // provide form to filter categories by number of entries P4Cms_PubSub::subscribe('p4cms.category.grid.form.subForms', function(Zend_Form $form) { $options = array( '' => 'Any Number', 'more' => 'One or More', 'none' => 'None' ); $form = new P4Cms_Form_SubForm; $form->setName('entriesCount') ->setAttrib('class', 'types-form') ->setOrder(20) ->addElement( 'Radio', 'display', array( 'label' => 'Number of Entries', 'multiOptions' => $options, 'autoApply' => true, 'order' => 20, 'value' => '' ) ); return $form; } ); // touch up the query to reflect our display of display empty categories P4Cms_PubSub::subscribe('p4cms.category.grid.populate', function(P4Cms_Model_Iterator $categories, Zend_Form $form) { $values = $form->getValues(); // extract selected types. $display = isset($values['entriesCount']['display']) ? $values['entriesCount']['display'] : ''; // early exit if no display restrictions if (!$display) { return null; } // helper function to determine whether the category passes the display filter // @return boolean true if category passes the filter, false otherwise $passesFilter = function (Category_Model_Category $category, $display) { return ($display === 'more' && $category->getEntries()) || ($display === 'none' && !$category->getEntries()); }; // get list with categories matching the search query $categories->filterByCallback($passesFilter, $display); } ); // subscribe to the record query topic P4Cms_PubSub::subscribe('p4cms.content.record.query', function(P4Cms_Record_Query $query, P4Cms_Record_Adapter $adapter) { // are we filtering by categories? $filter = $query->getFilter(); if (!$filter) { return; } $categoryIds = $filter->getOption('categories'); if (!$categoryIds || (!is_string($categoryIds) && !is_array($categoryIds))) { return; } if (is_string($categoryIds)) { $categoryIds = (array) $categoryIds; } // get entries for selected category ids. $ids = array(); foreach ($categoryIds as $category) { try { $category = Category_Model_Category::fetch($category); $ids = array_merge($ids, $category->getEntries()); } catch (P4Cms_Model_NotFoundException $e) { continue; } } // filter query to matching entries. $query->addPaths($ids, true); } ); // organize category records when pulling changes. P4Cms_PubSub::subscribe( 'p4cms.site.branch.pull.groupPaths', function($paths, $source, $target, $result) { $paths->addSubGroup( array( 'label' => 'Categories', 'basePaths' => $target->getId() . '/categories/...', 'inheritPaths' => $target->getId() . '/categories/...', 'pullByDefault' => true, 'count' => function($group, $count, $options) use ($target) { $categories = array_map( 'dirname', $group->getPaths($options)->invoke('getValue', array('depotFile')) ); return count(array_unique($categories)); }, 'details' => function($paths) use ($source, $target) { $ids = array(); $details = new P4Cms_Model_Iterator; foreach ($paths as $path) { $entry = basename($path->depotFile); $category = dirname($path->depotFile) . '/' . Category_Model_Category::CATEGORY_FILENAME; $id = Category_Model_Category::depotFileToId($category); // add some defaults for this category if this is the // first time we have seen it if (!isset($details[$id])) { $ids[] = $id; $details[$id] = new P4Cms_Model( array( 'conflict' => false, 'action' => 'edit' ) ); } // if we have the actual category index file we should // use its action for our details entry if ($entry == Category_Model_Category::CATEGORY_FILENAME) { $details[$id]->setValue('action', $path->action); } // if any of the paths in this entry are in conflict the // whole category needs to be flagged as a conflict if ($path->conflict) { $details[$id]->setValue('conflict', $path->conflict); } } // fetch all of the categories in one go and set their // titles onto the associated details entries $entries = Site_Model_PullPathGroup::fetchRecords( $ids, 'Category_Model_Category', $source, $target ); foreach ($entries as $entry) { $details[$entry->getId()]->setValue('label', $entry->getTitle()); } $details->setProperty( 'columns', array('label' => 'Category', 'action' => 'Action') ); return $details; } ) ); } ); } /** * Expand a dynamic category menu item. * * @param P4Cms_Navigation_Page_Dynamic $item the dynamic item to be expanded. * @param array $options options (hints) to influence expansion. * @return array the replacement menu items. */ public static function expandDynamicMenu($item, $options) { // if current user is not allowed to access categories, return empty array. if (P4Cms_User::hasActive()) { $user = P4Cms_User::fetchActive(); if (!$user->isAllowed('categories', 'access')) { return array(); } } // if options specify max-items, only fetch the specified number of records. $query = new P4Cms_Record_Query; $query->setSortBy(array('title')); // if options specify max-depth, only fetch up to max depth. if ($options[P4Cms_Menu::MENU_MAX_DEPTH] !== null) { $query->setMaxDepth($options[P4Cms_Menu::MENU_MAX_DEPTH]); } // if options specify menu root, only search under the given path. $root = $options[P4Cms_Menu::MENU_ROOT]; if ($root) { // increment max-depth to account for added depth of root. if ($query->getMaxDepth() !== null) { $query->setMaxDepth($query->getMaxDepth() + substr_count($root, '/') + 1); } // make the root inclusive, outside filtering needs the specified root to // be present to make it happy. The root itself will be removed by the caller. $query->addPaths(array($root, $root .'/...')); } // take the flat list of all categories and transform it into // a multi-dimensional array of navigation page entries. $categories = Category_Model_Category::fetchAll($query); $container = array(); foreach ($categories as $category) { // scan down the list of ancestors and create empty entries for any missing ones $parentContainer =& $container; foreach ($category->getAncestorIds() as $ancestorId) { // skip any ancestor IDs which are not under our filtered root if ($root && strpos($ancestorId, $root) !== 0) { continue; } // make a stub entry if we haven't found the ancestor yet // this entry will be replaced when we later encounter it if (!array_key_exists($ancestorId, $parentContainer)) { $parentContainer[$ancestorId] = array('pages' => array()); } // keep drilling down on the parentContainer pointer $parentContainer =& $parentContainer[$ancestorId]['pages']; } // if we have a 'stub' entry, cache the pages and remove it; // removing it ensures the correct order is maintained. $id = $category->getId(); $pages = array(); if (isset($parentContainer[$id]['pages'])) { $pages = $parentContainer[$id]['pages']; unset($parentContainer[$id]); } // add the new entry; maintaining child pages had they been present $parentContainer[$id] = array( 'label' => $category->getTitle(), 'expansionId' => $category->getId(), 'pages' => $pages, 'module' => 'category', 'controller' => 'index', 'action' => 'index', 'route' => 'category', 'encode' => false, 'params' => array( 'category' => $category->getId() ) ); // update pointer for any entries we may add $parentContainer =& $parentContainer[$id]['pages']; // include content entries if so configured. $options += array('includeEntries' => false); if ($options['includeEntries'] || $item->get('includeEntries')) { $entriesOptions = array(Category_Model_Category::OPTION_DEREFERENCE => true); foreach ($category->getEntries($entriesOptions) as $entry) { $entryId = $category->getId() . '/' . $entry->getId(); $parentContainer[$entryId] = array( 'label' => $entry->getTitle(), 'uri' => $entry->getUri(), 'expansionId' => $entryId ); } } } return $container; } }