/ */ class Category_ManageController extends Zend_Controller_Action { public $contexts = array( 'add' => array('partial' => 'get', 'json' => 'post'), 'edit' => array('partial' => 'get', 'json' => 'post'), 'delete' => array('partial' => 'get', 'json' => 'post'), 'index' => array('json'), 'move' => array('json' => 'post') ); /** * Use management layout for all actions. */ public function init() { $this->getHelper('layout')->setLayout('manage-layout'); } /** * List categories. * * @publishes p4cms.category.grid.actions * Modify the passed menu (add/modify/delete items) to influence the actions shown * on entries in the Manage Categories grid. * P4Cms_Navigation $actions A menu to hold grid actions. * * @publishes p4cms.category.grid.data.item * Return the passed item after applying any modifications (add properties, change * values, etc.) to influence the row values sent to the Manage Categories grid. * array $item The item to potentially modify. * mixed $model The original object/array that was used * to make the item. * Ui_View_Helper_DataGrid $helper The view helper that broadcast this * topic. * * @publishes p4cms.category.grid.data * Adjust the passed data (add properties, modify values, etc.) to influence * the row values sent to the Manage Categories grid. * Zend_Dojo_Data $data The data to be filtered. * Ui_View_Helper_DataGrid $helper The view helper that broadcast this * topic. * * @publishes p4cms.category.grid.populate * Adjust the passed iterator (possibly based on values in the passed form) to * filter which categories will be shown on the Manage Categories grid. * P4Cms_Model_Iterator $items An iterator of Category_Model_Category * objects. * P4Cms_Form_PubSubForm $form A form containing filter options. * * @publishes p4cms.category.grid.render * Make adjustments to the datagrid helper's options pre-render (e.g. change * options to add columns) for the Manage Categories grid. * Ui_View_Helper_DataGrid $helper The view helper that broadcast this * topic. * * @publishes p4cms.category.grid.form * Make arbitrary modifications to the Manage Categories filters form. * P4Cms_Form_PubSubForm $form The form that published this event. * * @publishes p4cms.category.grid.form.subForms * Return a Form (or array of Forms) to have them added to the Manage Categories * filters form. The returned form(s) should have a 'name' set on them to allow * them to be uniquely identified. * P4Cms_Form_PubSubForm $form The form that published this event. * * @publishes p4cms.category.grid.form.preValidate * Allows subscribers to adjust the Manage Categories filters form prior to * validation of the passed data. For example, modify element values based on * related selections to permit proper validation. * P4Cms_Form_PubSubForm $form The form that published this event. * array $values An associative array of form values. * * @publishes p4cms.category.grid.form.validate * Return false to indicate the Manage Categories filters form is invalid. Return * true to indicate your custom checks were satisfied, so form validity should be * unchanged. * P4Cms_Form_PubSubForm $form The form that published this event. * array $values An associative array of form values. * * @publishes p4cms.category.grid.form.populate * Allows subscribers to adjust the Manage Categories filters form after it has * been populated with the passed data. * P4Cms_Form_PubSubForm $form The form that published this event. * array $values The values passed to the populate * method. */ public function indexAction() { // enforce permissions. $this->acl->check('categories', 'manage'); // setup list options form $request = $this->getRequest(); $gridNamespace = 'p4cms.category.grid'; $form = new Ui_Form_GridOptions( array( 'namespace' => $gridNamespace ) ); $form->populate($request->getParams()); // set up view $view = $this->view; $view->form = $form; $view->pageSize = $request->getParam('count', 100); $view->rowOffset = $request->getParam('start', 0); $view->pageOffset = round($view->rowOffset / $view->pageSize, 0) + 1; $view->showAddLink = $this->acl->isAllowed('categories', 'add'); $view->headTitle()->set('Manage Categories'); // set DataGrid view helper namespace $helper = $view->dataGrid(); $helper->setNamespace($gridNamespace); // collect the actions from interested parties $actions = new P4Cms_Navigation; P4Cms_PubSub::publish($gridNamespace . '.actions', $actions); $view->actions = $actions; // early exit for standard requests (ie. not json) if (!$this->contextSwitch->getCurrentContext()) { $this->getHelper('helpUrl')->setUrl('navigation.category.overview.html'); return; } // fetch categories and make a copy for later use $categories = Category_Model_Category::fetchAll(); $copy = new P4Cms_Model_Iterator($categories->getArrayCopy()); // allow third-parties to influence list try { P4Cms_PubSub::publish($gridNamespace . '.populate', $categories, $form); } catch (Exception $e) { P4Cms_Log::logException("Error building categories list.", $e); } // restore categories hierarchy by appending ancestors to the // filtered categories $this->_restoreCategoriesHierarchy($categories, $copy); // prepare sorting options $sortKey = $request->getParam('sort', 'title'); $sortFlags = array( P4Cms_Model_Iterator::SORT_NATURAL, P4Cms_Model_Iterator::SORT_NO_CASE ); if (substr($sortKey, 0, 1) == '-') { $sortKey = substr($sortKey, 1); $sortFlags[] = P4Cms_Model_Iterator::SORT_DESCENDING; } else { $sortFlags[] = P4Cms_Model_Iterator::SORT_ASCENDING; } // compose list of sorted categories $view->categories = $this->_sortCategoriesRecursively($categories, $sortKey, $sortFlags); } /** * Add a category. */ public function addAction() { // enforce permissions. $this->acl->check('categories', 'add'); // set up view $view = $this->view; $request = $this->getRequest(); $view->shortForm = $request->getParam('short', false); $view->headTitle()->set('Add Category'); // set up form $form = new Category_Form_Manage; $view->form = $form; $form->setIdPrefix($request->getParam('formIdPrefix')); if ($request->isPost()) { // if a valid data were posted, try to add the category if ($form->isValid($request->getPost())) { // populate the form with posted data $form->populate($request->getPost()); // create new category with collected values and save it. // try to add a category with posted data; it may fail when // ancestors don't exist $category = new Category_Model_Category; try { $category->setValues($form->getValues()) ->setId($form->composeCategoryId()) ->save(); } catch (InvalidArgumentException $e) { // handle the category ancestry does not exist exception if ($e->getMessage() == 'Cannot create new category; category ancestry does not exist.') { $form->getElement('parent')->addError( "Cannot create new category; category ancestor '" . $category->getParentId() . "' does not exist." ); } else { // re-thrown other exceptions throw $e; } } } // re-check whether form is valid (i.e. doesn't contain any error messages) if ($form->getMessages()) { // form is invalid; set bad request response code // and assign error messages to the view $this->getResponse()->setHttpResponseCode(400); $view->errors = $form->getMessages(); return; } // if we reached this point, form is valid; // set notification message $view->message = "Category '{$category->getTitle()}' has been successfully added."; // for traditional requests, add notification message and redirect if (!$this->contextSwitch->getCurrentContext()) { // notify user and return to category list. P4Cms_Notifications::add( $view->message, P4Cms_Notifications::SEVERITY_SUCCESS ); $this->redirector->gotoUrl($request->getBaseUrl()); } // assign details about new category to the view $view->category = $category; } } /** * Edit a content category. * * @param array $params The values to apply */ public function editAction(array $params = null) { // enforce permissions. $this->acl->check('categories', 'manage'); // set up view $view = $this->view; $view->headTitle()->set('Edit Category'); // fetch the content category to be edited. $request = $this->getRequest(); $id = $request->getParam('category'); $category = Category_Model_Category::fetch($id); // setup form $form = new Category_Form_Manage; $view->form = $form; $values = array_merge( $category->getValues(), array( 'parent' => $category->getParentId() ) ); $form->populate($values); // disable unique title check $form->setUniqueTitleRequired(false); // remove the current category and it's children from parent options. $newOptions = array(); foreach ($form->getElement('parent')->getMultiOptions() as $key => $label) { if ($key === $id or strpos($key, "$id/") === 0) { continue; } $newOptions[$key] = $label; } $form->getElement('parent')->setMultiOptions($newOptions); if ($request->isPost()) { // if params is specified, we use them to override the existing values // else item values come from request if (isset($params)) { $values = array_merge($category->getValues(), $params); } else { $values = $request->getPost(); } // if a valid data were posted, try to update the category if ($form->isValid($values)) { // populate the form with posted data $form->populate($values); // start a batch in case we're moving $adapter = $category->getAdapter(); $adapter->beginBatch("Edit category $id"); // if the id has changed, move the category $newId = $form->composeCategoryId(); if ($id !== $newId) { try { Category_Model_Category::move($id, $newId); } catch (InvalidArgumentException $e) { $form->getElement('parent')->addError($e->getMessage()); } } // set form values on category, and save it try { $category->setValues($form->getValues()) ->setId($newId) ->save(); } catch (InvalidArgumentException $e) { // handle the category ancestry does not exist exception if ($e->getMessage() == 'Cannot create new category; category ancestry does not exist.') { $form->getElement('parent')->addError( "Cannot create new category; category ancestor '" . $category->getParentId() . "' does not exist." ); } // re-throw other exceptions throw $e; } } // re-check whether form is valid (i.e. doesn't contain any error messages) if ($form->getMessages()) { // form is invalid; revert batch, set bad request response code // and assign error messages to the view $this->getResponse()->setHttpResponseCode(400); $view->errors = $form->getMessages(); if (isset($adapter)) { $adapter->revertBatch(); } return; } // at this point, form is valid; // attempt to commit the batch - cleanup on failure. try { $adapter->commitBatch(); } catch (Exception $e) { $adapter->revertBatch(); throw $e; } // set notification message $view->message = "Category '{$category->getTitle()}' has been updated."; // for traditional requests, add notification message and redirect if (!$this->contextSwitch->getCurrentContext()) { P4Cms_Notifications::add( $view->message, P4Cms_Notifications::SEVERITY_SUCCESS ); $this->redirector->gotoSimple('index'); } } } /** * Move a category */ public function moveAction() { // enforce permissions. $this->acl->check('categories', 'manage'); $request = $this->getRequest(); $values = array("parent" => $request->getParam('parent')); $this->editAction($values); } /** * Remove a content category. */ public function deleteAction() { // enforce permissions. $this->acl->check('categories', 'manage'); // require post request method. $request = $this->getRequest(); if (!$request->isPost()) { throw new P4Cms_AccessDeniedException( "Cannot delete category. Request method must be http post." ); } $id = $request->getParam('id', $request->getParam('category')); $category = Category_Model_Category::fetch($id); $category->delete(); $this->view->category = $category; } /** * Sort the given list of categories recursively to preserve parent-child * relationships. * * Example: * * Assume there are 2 categories, A, B where A has 2 childs A/B, A/C. * The output is an array iterator with categories in the following order: * - A, A/B, A/C, B if sorted A to Z * - B, A, A/C, A/B if sorted Z to A * * @param P4Cms_Model_Iterator $categories list of all categories * used for getting the children * @param string|array $sortKey key to sort categories by * @param array $sortFlags sorting options * @param string|null $parentId optional - parent category of * the generated list * (null by default) * @return P4Cms_Model_Iterator list of sorted categories */ protected function _sortCategoriesRecursively($categories, $sortKey, $sortFlags, $parentId = null) { // get categories with given parent and sort them $childs = $categories ->filterByCallback( function($category, $parentId) { return $category->getParentId() === $parentId; }, $parentId, array(P4_Model_Iterator::FILTER_COPY) ) ->sortBy($sortKey, $sortFlags); // assemble categories list and append sorted sub-categories below each category $sortedCategories = new P4Cms_Model_Iterator; foreach ($childs as $category) { $sortedCategories[] = $category; $sortedChilds = $this->_sortCategoriesRecursively( $categories, $sortKey, $sortFlags, $category->getId() ); foreach ($sortedChilds as $childCategory) { $sortedCategories[] = $childCategory; } } return $sortedCategories; } /** * Restore categories hierarchy by appending ancestors to the items contained * in match so as for any sub-category, all its ancestors are also present. * Parent categories that do not match, but must be included, are refered as * obligatory. * In addition, these obligatory items will be added an extra 'obligatory' * field so they can be recognized. * * @param P4Cms_Model_Iterator $match list with categories to keep * @param P4Cms_Model_Iterator $categories list with categories where * ancestors will be taken from */ protected function _restoreCategoriesHierarchy(P4Cms_Model_Iterator $match, P4Cms_Model_Iterator $categories ) { // compose list with categories to keep $keepIds = array(); foreach ($match as $category) { $keepIds = array_merge( $keepIds, array($category->getId()), $category->getAncestorIds() ); } // make keep list unique as there may be duplicates $keepIds = array_unique($keepIds); // filter categories to keep only selected items $categories->filter('id', $keepIds); // append and mark obligatory categories $obligatory = array_diff($keepIds, $match->invoke('getId')); foreach ($categories as $category) { if (in_array($category->getId(), $obligatory)) { $category->setValue('obligatory', true); $match->append($category); } } } }