I'm building a website that contains a product database. Each product belongs to a category. The structure of categories is multi-tiered and can contain any number of tiers, for example:
Electronics > Games Consoles > Xbox > Xbox One > Games > etc..
Fashion > Mens > Shirts > Long Sleeved
I always assign the product to the "last" category in the tier.
Here is the structure of my category table:
id name parent_id
================================
1 Fashion NULL
2 Mens 1
3 Shirts 2
4 Long Sleeved 3
5 Short Sleeved 3
I'm using Yii2 as my application framework, but the same concepts should apply to most MVC Frameworks, or at least those that implement an ORM like ActiveRecord.
What I want to do is:
For any category level, get the "master" parent. I.e. for Shirts it would be Fashion
For any category level, get all "last" level categories in the tier. I.e. for Mens it would be Long Sleeved and Short Sleeved.
(more advanced) For any category level, find out the number of children / parents it has.
I have the following default relations in my model:
public function getParent()
{
return $this->hasOne(Category::className(), ['id' => 'parent_id']);
}
public function getParent()
{
return $this->hasMany(Category::className(), ['parent_id' => 'id']);
}
The following is a function I have created which outputs the "tree" for any given category:
public function getParentTree()
{
$array = [];
// $this->parent refers to the 'getParent()' relation above
if(!empty($this->parent))
{
$array[] = $this->parent->name;
if(!empty($this->parent->parent))
$array[] = $this->parent->parent->name;
if(!empty($this->parent->parent->parent))
$array[] = $this->parent->parent->parent->name;
}
else
$array[] = "(none)";
$output = implode(" --> ", array_reverse($array));
return $output;
}
But there is a lot of repetition here and it looks ugly. But it is also leading me to be believe perhaps I have taken the wrong approach and need to restructure the database itself?
Bill I think I have resolved this issue in YII2 -> Models.
Below is my code.
public static function getSubCategories($parent_id = NULL, $level = 0)
{
// Get the Category from table
// Here you can use caching Yii::$app->cache->get to avoid multiple queries
$categories = Category::find()->select(['id', 'parent_id', 'name'])->where(['parent_id' => $parent_id])->asArray()->all();
// Logic of Nth level to return
self::$max_down_level += 1;
if($level != 0 && self::$max_down_level > $level) return $categories;
// Now for each sub categories find and return chidren as Array
foreach($categories as $key => $category)
{
$categories[$key]['children'] = self::getSubCategories($category['id'], $level);
}
return $categories;
}
Also do not forget to declare public static $max_down_level = 0; variable in your model class. and now call the function like below.
To get all the children of Parent category self::getSubCategories(NULL)
To get all the children up to 2nd level self::getSubCategories(NULL, 2)
Same way above you can declare recursive function for getting parent Categories.
public static function getParentCategories($parent_id, $level = 0)
{
// Get the Category from table
// Here you can use caching Yii::$app->cache->get to avoid multiple queries
$categories = Category::find()->select(['id', 'parent_id', 'name'])->where(['id' => $parent_id])->asArray()->all();
// Logic of Nth level to return
self::$max_up_level += 1;
if($level != 0 && self::$max_up_level > $level) return $categories;
foreach($categories as $key => $category)
{
$categories[$key]['parent'] = self::getParentCategories($category['parent_id'], $level);
}
return $categories;
}
Also do not forget to declare public static $max_up_level = 0; variable in your model class. and now call the function like below.
To get all the children of Parent category self::getParentCategories(16, 0)
To get all the children up to 2nd level self::getParentCategories(16, 2)
You can use your own class name instead of self
Hope this helps.
Related
Below lists the output of a mySQL query in PHP PDO. The object contains multiple columns from two tables that are then to be combined into a single object.
Some rows in the same table are children of others as identified by the column parent_ID. These children then need to be added to the object of the parent as do their children and so on and so on.
As much as I can achieve this simply for the first two levels of children I cannot see a way without performing another foreach to achieve this beyond the first to layers of the object.
This example should add clarity to the above:
foreach($components as $component){
if($component->parent_ID < 0){
$output->{$component->ID} = $component;
}
else if($output->{$content->parent_ID}){
$output->{$content->parent_ID}->child->{$component->ID} = $component;
}
else if($output->?->child->{$conent->parent_ID}){
$output->?->child->{$content->parent_ID}->child->{$component->ID} = $component;
}
}
Not on the third line there is an ? where there would normally be an ID. This is because we now do not know what that ID is going to be. In the first layer we did because it would be the parent_ID but this line is dealing with the children of children of a parent.
So, as I far I understood from comments and assuming that you don't have a lot of records in DB, it's seems to me the best way is to preload all rows from DB and then build a tree using this function
public function buildTree(array &$objects) {
/** thanks to tz-lom */
$index = array();
$relations = array();
foreach($objects as $key => $object) {
$index[$object->getId()] = $object->setChildren(array());
$relations[$object->getParentId()][] = $object;
if ($object->getParentId()) {
unset($objects[$key]);
}
}
foreach ($relations as $parent => $children) {
foreach ($children as $_children) {
if ($parent && isset($index[$parent])) {
$index[$parent]->addChildren($_children->setParent($index[$parent]));
}
}
}
return $this;
}
P.S. Really, I don't see other way without foreach in foreach. At least, it's not recursive
I have a quite unique problem.
I have a shop where there are multiple categories in a setup like this
Collection
.... Shorts (products: small 16 - RED and small 20 - BLUE)
.... Dress (products: blue: 16 , Green 19)
If I open Collection in the shop I get the items like this
Blue 16
Green 19
small 16 - RED
small 20 - BLUE
I want my output to be like this:
small 16 - RED
small 20 - BLUE
Blue 16
Green 19
How can i get this results? I'm sorry i haven't provided any code, as i have no idea how i should achieve this
I do something similar to this.
Within Magento admin you can manually set the order in which the products display on the category page.
Catalog -> Manage Categories (select your category)
Under the "Category Products" tab you will see a table containing all the products assigned to the category, on the far right there is a column called "Position". Here is where you enter an int value, the lower the number the higher the product would appear on the category page.
1 create observer on catalog_block_product_list_collection event
<events>
<catalog_block_product_list_collection>
<observers>
<namespace_module>
<class> namespace_module/observer</class>
<method>collectionList</method>
</namespace_module >
</observers>
</catalog_block_product_list_collection>
</events>
2 create class Namespace_Module_Model_Observer
class Namespace_Module_Model_Observer
{
public function collectionList($observer)
{
/** #var Mage_Catalog_Model_Category $currentCategory */
$currentCategory = Mage::registry('current_category');
$children = Mage::getResourceModel('catalog/category')->getChildrenIds($currentCategory);
if (!$children) {
return $this;
}
$children = implode(',', $children);
/** #var Mage_Catalog_Model_Resource_Product_Collection $collection */
$collection = $observer->getCollection();
$attr = $this->_getAttribute('name');
$collection->getSelect()
->join(
array('c' => $this->_getResource()->getTableName('catalog_category_product')),
"c.product_id = e.entity_id AND c.category_id IN ($children)",
array('child_category_id' => 'category_id')
)
->join(
array('ac' => $this->_getResource()->getTableName('catalog_category_entity_' . $attr['backend_type'])),
"c.category_id = ac.entity_id AND ac.attribute_id = {$attr['attribute_id']}",
array('child_category_name' => 'value')
)
->order('child_category_name DESC');
return $this;
}
protected function _getAttribute($attributeCode, $static = true, $entityTypeId = 3)
{
$readAdapter = $this->_getReadAdapter();
$select = $readAdapter->select()
->from($this->_getResource()->getTableName('eav/attribute'))
->reset(Zend_Db_Select::COLUMNS)
->columns(array('attribute_id', 'backend_type'))
->where('entity_type_id = ?', $entityTypeId)
->where('attribute_code = ?', $attributeCode)
->limit(1);
if (!$static) {
$select->where('backend_type != ?', 'static');
}
$entityId = $readAdapter->query($select)->fetch();
return $entityId;
}
protected function _getResource()
{
return Mage::getSingleton('core/resource');
}
protected function _getReadAdapter()
{
return $this->_getResource()->getConnection('core_read');
}
}
here we set collection sort by child category name, you can change it to category id or add to collection any category attribute and sort by this attribute
->order('child_category_name DESC');
this is just sample how quickly sort product collection by child categories, of course you can add option in toolbar and sort collection dynamically
I think you should create an attribute from admin,
Create an attribute custom_order from Admin->Catalog->Attributes->Manage Attributes.
set Used in Product Listing = Yes
Used for Sorting in Product Listing = Yes
Assign the position value for each product Individually .
Then go to Admin->Catalog->Manage Categories.
select a category, click on Display settings tab,
set "Default Product Listing Sort By" custom_order
Using CActiveRecord my table looks like this:
A column parent_id has relation many to id, and it works properly.
id | parent_id
---+----------
1 1 <- top
2 1 <- means parent_id with 1 has parent with id=1
3 1
4 2 <- parent for is id=2
5 2
6 2 and so many nested levels....
A goal is how to properly get nested as PHP classically way nested arrays data (arrays inside arrays).
array(1,1) {
array(2,1) {
array(4,2) ....
}
}
Problem is Yii. I didn't find properly way how to pick up a data as nested array using properly CActiveRecord.
What is best way to make nested array results? A main goal is to easy forward to render view so I don't separate with too many functions and calling many models outside from modules or models.
A good is one function to get a result.
Solved using this: Recursive function to generate multidimensional array from database result
You need get a data as arrays from model:
$somemodel = MyModel::model()->findAll();
Then put all in array rather then Yii objects model or what you need:
foreach ($somemodel as $k => $v)
{
$arrays[$k] = array('id' => $v->id, 'parent_id' => $v->parent_id, 'somedata' => 'Your data');
}
Then call a function:
function buildTree(array $elements, $parentId = 0) {
$branch = array();
foreach ($elements as $element) {
if ($element['parent_id'] == $parentId) {
$children = buildTree($elements, $element['id']);
if ($children) {
$element['children'] = $children;
}
$branch[] = $element;
}
}
return $branch;
}
Then call put all $arrays data into buildTree function to rebuild nesting arrays data.
$tree = buildTree($arrays);
Now your $tree is nested arrays data.
Note: there aren't depth into function but in convient way you can add using like this sample: Create nested list from Multidimensional Array
Here's how I'm getting my data from database. It is a 3 level hierarchy. Parent, child and children of child.
page_id | parent_id
1 0
2 1
3 0
4 3
The top data shows 0 as the parent.
$id = 0;
public function getHierarchy($id)
{
$Pages = new Pages();
$arr = array();
$result = $Pages->getParent($id);
foreach($result as $p){
$arr[] = array(
'title' => $p['title'],
'children' => $this->getHierarchy($p['page_id']),
);
}
return $arr;
}
So far I'm getting data but it's kinda slow. The browser loads so long before showing the data. How can I make my code faster so it does not take long for the browser to load data?
Thanks.
You have multiple options to speed it up:
Select all rows from table and do the recursion only on the PHP
side. It's only an option if there are not too many rows in table.
You can self join the table. This is a nice option if you know that there are no more levels of hierarchy in the future. Look at the post: http://blog.richardknop.com/2009/05/adjacency-list-model/
Use nested sets: http://en.wikipedia.org/wiki/Nested_set_model
Or my favorite: Closure Table
Use some kind of caching. You could cache the db queries or the whole tree menu html or the PHP array for example.
Caching example:
Caching you can do in normal files or use specialized api's like memcache, memcached or apc.
Usually you have a class with 3 basic methods. An interface could look like:
interface ICache
{
public function get($key);
public function set($key, $value, $lifetime = false);
public function delete($key);
}
You can use it like:
public function getHierarchy($id)
{
$cached = $this->cache->get($id);
if (false !== $cached) {
return $cached;
}
// run another method where you build the tree ($arr)
$this->cache->set($id, $arr, 3600);
return $arr;
}
In the example the tree will be cached for one hour. You also could set unlimited lifetime and delete the key if you insert another item to the db.
Im doing a project in php CodeIgniter which has a table where all attributes_values can be kept and it is designed such that it can have its child in same tbl. the database structure is
fld_id fld_value fld_attribute_id fld_parent_id
1 att-1 2 0
2 att-2 2 0
3 att-1_1 2 1
4 att-1_2 2 1
5 att-1_1_1 2 3
here above att-1 is the attribute value of any attribute and it has two child att-1_1 and att-1_2 with parent id 1. and att-1_1 has too its child att-1_1_1 with parent_id 3. fld_parent_id is the fld_id of the same table and denotes the child of its. Now i want to show this in tree structure like this
Level1 level2 level3 ..... level n
att-1
+------att-1_1
| +------att-1_1_1
+------att-1_2
att-2
and this tree structure can vary upto n level. the attribute values with parent id are on level one and i extracted the values from level one now i have to check the child of its and if it has further child and display its child as above. i used a helper and tired to make it recursive but it didnt happen. So how could i do it such: the code is below
foreach($attributes_values->result() as $attribute_values){
if($attribute_values->fld_parent_id==0 && $attribute_values->fld_attribute_id==$attribute->fld_id){
echo $attribute_values->fld_value.'<br/>';
$children = get_children_by_par_id($attribute_values->fld_id); //helper function
echo '<pre>';
print_r($children);
echo '</pre>';
}
}
and the helper code is below:
function get_children_by_par_id($id){ //parent id
$children = get_children($id);
if($children->num_rows()!=0){
foreach($children->result() as $child){
get_children_by_par_id($child->fld_id);
return $child;
}
}
}
function get_children($id){
$CI = get_instance();
$CI->db->where('fld_parent_id',$id);
return $CI->db->get('tbl_attribute_values');
}
please help me...............
The key of recursion is an "endless" call. This can be done with a function that calls it self.
So
function get_children($parent_id)
{
// database retrieve all stuff with the parent id.
$children = Array();
foreach($results as $result)
{
$result['children'] = get_children($result['id']);
$children[] = $result;
}
return $children;
}
Or use the SPL library built into PHP already PHP recursive iterator