Skip to content

Instantly share code, notes, and snippets.

@brianally
Created July 2, 2015 20:41
Show Gist options
  • Save brianally/19657bf31dfb744dd676 to your computer and use it in GitHub Desktop.
Save brianally/19657bf31dfb744dd676 to your computer and use it in GitHub Desktop.
Hierarchical categories using MPTT with CakePHP 2.x
<?php
class Category extends AppModel {
public $hasAndBelongsToMany = [
'Item' => [
'className' => 'Item',
'with' => 'CategoryItem',
'foreignKey' => 'category_id',
'associationForeignKey' => 'item_id'
]
];
public $hasMany = [
'SubCategory' => [
'className' => 'Category',
'foreignKey' => 'parent_id',
'order' => ['SubCategory.lft' =>'ASC']
],
'CategoryItem'
];
public $actsAs = [
'Tree' => [
'parent' => 'parent_id',
'left' => 'lft',
'right' => 'rgt'
),
'Slugged' => [
'label' => 'name',
'case' => 'low',
'overwrite' => true,
'length' => 64,
'unique' => false,
'replace' => array(
'&' => 'and',
'+' => 'and',
'#' => 'hash',
'.' => '-',
'/' => '-',
',' => ''
]
]
];
public $validate = [
'name' => [
'provided' => [
'rule' => ['minLength', 1],
'required' => true,
'message' => 'no name for category'
]
]
];
public function beforeSave($options = []) {
if ( $this->data[$this->alias]['parent_id'] ) {
$path = $this->field(
'slug_path',
[$this->alias.'.'.$this->primaryKey => $this->data[$this->alias]['parent_id'] ]
);
$this->data[$this->alias]['slug_path'] = $path . '_' . $this->data[$this->alias]['slug'];
} else {
$this->data[$this->alias]['slug_path'] = $this->data[$this->alias]['slug'];
}
}
public function afterSave( $created, $options = [] ) {
Cache::clear();
}
function beforeDelete($cascade = true) {
return ( !$this->hasChildren($this->id) && !$this->hasItems($this->id) );
}
function afterDelete() {
Cache::clear();
}
/**
* get the navigation tree
*
* @return array data
*/
public function getNav() {
$data = $this->find(
'threaded',
[
'order'=> [$this->alias.'.lft' =>'ASC'],
'recursive' => -1,
'contain' => [
'Item' => [
'fields' => ['Item.id', 'Item.inactive']
]
]
]
);
// the output of this method will be cached
$data = array_map(
[$this->name, 'reduceItemsToCount')]
$data
);
return $data;
}
public function toJson($arr, $stack = [] ) {
$set = [];
foreach($arr as $datum) {
$stack[] = $datum['Category']['name'];
$out = [
'name' => $datum['Category']['name'],
'slug' => $datum['Category']['slug'],
'title_path' => implode(',', $stack),
'slug_path'=> strtr( $datum['Category']['slug_path'], '_', ',' )
];
if ( !empty($datum['children']) ) {
$out['children'] = toJson($datum['children'], $stack);
}
$set[] = $out;
array_pop($stack);
}
return $set;
}
/**
* Static callback to remove elements from a Category's Item array
* to a simple count, or remove it altogether.
*
* @param mixed $node element passed by array_map()
* @return mixed data
*/
private static function reduceItemsToCount($node, $parent_id = null) {
if (isset($node['Item'])) {
// remove inactive Items
$node['Item'] = Hash::remove($node['Item'], '{n}[inactive=1]');
$size = sizeof($node['Item']);
if ($size) {
$node['Item'] = sizeof($node['Item']);
} else {
unset($node['Item']);
}
$node['parent_id'] = $parent_id;
if (isset($node['children'])) {
foreach( $node['children'] as $k => $child ) {
$node['children'][$k] = self::reduceItemsToCount($child, $node['parent_id']);
}
}
}
return $node;
}
private static function removeInactiveItems($el) {
if ( isset($el['inactive']) && $el['inactive'] ) {
return false;
}
return true;
}
/**
* Get all categories as a flat list
*
* @return array data
*/
public function getList() {
return $this->find(
'all',
array(
'fields' => array('slug', 'slug_path', 'name'),
'order' => array('id' => 'ASC'),
'recursive' => -1
)
);
}
/**
* Get a flat list of just the top-most categories
*
* @return array data
*/
public function fetchTopLevel() {
return $this->find(
'all',
[
'conditions' => [
$this->alias.'.parent_id' => null
]
]
);
}
/**
* get a single Category
*
* @param string $id model PK or slug
* @param boolean $manufacturers include Manufacturers for Items in this Category
* @return array data
*/
public function fetch($id, $manufacturers = false) {
if (empty($id)) return null;
$data = Cache::read("Category.${id}");
if (!empty($data)) return $data;
$filters = [];
if (is_int($id)) {
$filters['conditions'] = [$this->alias .'.'. $this->primaryKey => $id];
} else {
$filters['conditions'] = [$this->alias .'.slug_path' => $id];
}
if ($manufacturers) {
$filters['contain'] = [
'Item' => [
'fields' => [
'Item.id',
'Item.manufacturer_id',
'Item.name',
'Item.model'
],
'Manufacturer'
]
];
} else {
$filters['recursive'] = -1;
}
$data = $this->find(
'first',
$filters
);
$data['Manufacturer'] = Hash::sort(
$this->_arrayUnique(Hash::extract($data, 'Item.{n}.Manufacturer')),
'{n}.name',
'asc'
);
$data['Item'] = null;
unset($data['Item']);
Cache::write("Category.${id}", $data);;
return $data;
}
/**
* Check if category has child categories
*
* @param int $id parent category's id
* @return boolean
*/
public function hasChildren($id) {
$count = $this->find(
'count',
[
'conditions' => [
$this->alias.'.parent_id' => $id
]
]
);
return ($count > 0);
}
/**
* Check whether category has items
*
* @param [int $id
* @return boolean
*/
public function hasItems($id) {
$count = $this->CategoryItem->find(
'count',
[
'conditions' => [
'CategoryItem.category_id' => $id
]
]
);
return ($count > 0);
}
}
// in AppModel:
/**
* Create Unique Arrays using an md5 hash
*
* @param array $array
* @param boolean $preserveKeys
* @return array
* @access protected
* @link http://phpdevblog.niknovo.com/2009/01/using-array-unique-with-multidimensional-arrays.html
*/
protected function _arrayUnique($array, $preserveKeys = false) {
// Unique Array for return
$arrayRewrite = [];
if (sizeof($array)) {
// Array with the md5 hashes
$arrayHashes = [];
foreach($array as $key => $item) {
// Serialize the current element and create a md5 hash
$hash = md5(serialize($item));
// If the md5 didn't come up yet, add the element to
// to arrayRewrite, otherwise drop it
if (!isset($arrayHashes[$hash])) {
// Save the current element hash
$arrayHashes[$hash] = $hash;
// Add element to the unique Array
if ($preserveKeys) {
$arrayRewrite[$key] = $item;
} else {
$arrayRewrite[] = $item;
}
}
}
}
return $arrayRewrite;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment