bigin / ItemManager_2.0

ItemManager (IM) is a simple flat-file framework for GetSimple-CMS that allows you to develop completely customizable PHP applications bundled with GetSimple-CMS.
MIT License
5 stars 3 forks source link

How I can select Items from many categories? #12

Closed mittus closed 5 years ago

mittus commented 6 years ago

Can't select Items from many categories without fields on items.

I have dummy category, it have Field "direction", this field can be simultaneously at other categories. I create function, that search all categories with needed field. But I cant select all Items from all that categories without foreach in $categories. And I can't use filterSimpleItems and countItems at the same time with all needed Items.

function getCategories($direction) {

    $imanager = imanager();
    $itemMapper = $imanager->getItemMapper();
    $categoryMapper = $imanager->getCategoryMapper();

    $dummy = $imanager->getCategory(1);
    $itemMapper->alloc($dummy->id);
    $items = $itemMapper->getSimpleItems('active=1'.($direction?' && direction_value='.$direction:''));
    $categorySlug = '';
    foreach($items as $i => $item) {
        $categorySlug .= 'slug='.$item->slug;
        if($item->id !== end($items)->id) {
            $categorySlug .= ' && ';
        } else {
            $categories['direction'] = $item->direction;
        }
        $categoryId = $categoryMapper->getCategory('slug='.$item->slug)->id;
        $categories[$categoryId] = $imanager->getItemMapper()->findItem('slug='.$item->slug);
    }
    return $categories;
};

Can I use many categories on $itemMapper->alloc(); or other?

Please, help me with that task!

bigin commented 6 years ago

I don't know if I'm getting this right, it looks like you're trying to do something like a routing script that maps request to a specific items. If it is, then I don't know if this is the right way for this...

I would create an additional routing category instead, whose job it would be to provide the categories or item IDs on HTTP request.

bigin commented 6 years ago

If you want to search several categories for a certain field value you can either build your own function or use a native ItemMapper method findAll(). However, the findAll() supports standard Item and not SimpleItem objects, and returns a multidimensional array with Item objects grouped by categories. Note, since this method handle normal Item objects, it can become very slow depending on the number of categories/items. You can limit the number of categories to be searched by passing the specific IDs to the method, for example, to search only the categories with ID 3 and 7 you can call the method this way:

$itemsArray = $itemMapper->findAll('direction=foo', array(3, 7));

But since you work with SimpleItem objects, I would simply write my own method for it, which is usually very simple and faster.

bigin commented 6 years ago

Sorry, I don't really understand what you're trying to do.

No, you cannot load items from all categories into memory at the same time because there is no reason for it, but of course, you can merge them as you need:

$items = array();
foreach($categories as $category) {
    $itemMapper->alloc($category->id);
    $items = array_merge($items, $itemMapper->simpleItems);
}
mittus commented 6 years ago

Merge Items - it's what I needed!! This should be added to the documentation.

Thank you very much!!

bigin commented 6 years ago

Note, when you merging items, there can be issues, especially if you use the function filterSimpleItems(), because of the duplicate IDs of items from different categories. It's recommended to write your own function instead:

function filterSimpleItemsExt($filterby='position', $option='asc',  $offset=0, $length=0, array $locitems)
{
    $offset = ($offset > 0) ? (int) $offset-1 : (int) $offset;
    $length = (int) $length;

    if(empty($locitems)) return false;

    usort($locitems, function($a, $b) use ($filterby) 
    {
        $a = $a->$filterby;
        $b = $b->$filterby;
        if(is_numeric($a)) {
            if($a == $b) {return 0;}
            else {
                if($b > $a) {return -1;}
                else {return 1;}
            }
        } else {return strcasecmp($a, $b);}
    });

    if(strtolower($option) != 'asc') {
        $locitems = imanager()->getItemMapper()->reverseItems($locitems);
    }

    if(!empty($locitems) && ( $offset > 0 ||  $length > 0)) {
        if( $length == 0) $len = null;
        $locitems = array_slice($locitems,  $offset,  $length, true);
    }

    return $locitems;
}

So you can call that method this way:

$items = array();
foreach($categories as $category) {
    $itemMapper->alloc($category->id);
    $items = array_merge($items, $itemMapper->simpleItems);
}
$filteredItems = filterSimpleItemsExt('direction', 'DESC', 0, 0, $items);
bigin commented 6 years ago

Hmm, here's another thing, I don't think calling this line here in a foreach loop is a good idea, this could slow down the execution immensely:

$categories[$categoryId] = $imanager->getItemMapper()->findItem('slug='.$item->slug);

You should try to solve this differently way.

mittus commented 6 years ago

Thanks for your advice! Now I use this function for select and merge needed items:

/* search category id's */
$categories = array();

if($cat && empty($tag)) {
    if($cat != 'products') {
        /* selected category */
        $category = $imanager->getItemMapper()->findItem('slug='.$cat);
        array_push($categories, $category->id);
    } else {
        /* all categories */
        getCategories();
    }
} elseif($tag) {
    /* categories with direction_value */
    getCategories($tag);
};

function getCategories($direction) {
    global $categories;

    /* init */
    $imanager = imanager();
    $itemMapper = $imanager->getItemMapper();
    $categoryMapper = $imanager->getCategoryMapper();

    /* get dummy items */
    $dummy = $imanager->getCategory(1);
    $itemMapper->alloc($dummy->id);

    /* sort dummy items */
    $items = $itemMapper->getSimpleItems('active=1'.($direction?' && direction_value='.$direction:''));

    /* get category id's from dummy items slug */
    foreach($items as $i => $item) {
        $category = $categoryMapper->getCategory('slug='.$item->slug);
        array_push($categories, $category->id);
    }
};

/* select items */
if(!empty($categories)) {
    $items = array();
    $directionTitle;

    foreach($categories as $id) {
        /* get direction title */
        $category = $imanager->getCategory($id);
        if($category->id == end($categories) && !empty($tag)) {
            $categoryItem = $imanager->getItem(1, $category->id);
            $directionTitle = $categoryItem->fields->direction->value;
        }

        /* get all items from all selected categories with active=1 */
        $itemMapper->alloc($id);
        foreach($itemMapper->simpleItems as $key => $simpleItems) {
            if(!$simpleItems->active) {
                unset($itemMapper->simpleItems[$key]);
            }
        }
        $items = array_merge($items, $itemMapper->simpleItems);

    }
};

I can't use function getSimpleItems, what is the best way to sort items now?

bigin commented 6 years ago

Have you tried the function I posted? Or what exactly do you mean by "best way to sort items"?

bigin commented 6 years ago

Ahh, I think I know what you mean, do you mean a way to sort these items, right?

$items = array_merge($items, $itemMapper->simpleItems);

Why not, you can indeed use the getSimpleItems() function, but you have to call it before merging:

$selected = $itemMapper->getSimpleItems('product_type=fruits', 0, 0, $itemMapper->simpleItems);
if(is_array($selected)) {
    $items = array_merge($items, $selected);
}
bigin commented 6 years ago

By the way this also applies to the default filterSimpleItems() method, if you execute it before merge - it will work as well. The splitting of the items for pagination wouldn't work, but it should work fine with my function i posted before.

bigin commented 6 years ago

I was trying out a little bit, the problem with merging is that many native IM methods work with item IDs, overwriting the resulting array keys with the original item IDs. Because you have merged items from multiple categories, so there are ID duplicates, but no duplicate keys allowed in the array.

Funny, you can also execute native getSimpleItems() and filterSimpleItems after merge :-) But only if you don't work with item IDs and don't intend to save the items, so you can populate the IDs in ascending order:

$items = array_merge($items, $itemMapper->simpleItems);
if($items) {
    $i = 0;
    foreach($items as $key => $item) {
        $items[$key]->id = ++$i;
    }
}

...

$selected = $itemMapper->getSimpleItems('active=1', 0, 0, $items);

;-)

bigin commented 6 years ago

The biggest mistake when working with ItemManger is the use of functions for retrieving items without specifying a category ID. This includes, for example:

findItem(), findAll() or initAll() ...

You must always remember, if there is no category ID passed, all categories will be initialized sequentially to search the items for passed value. If you have many categories and items it can make the execution of the script very slow. Therefore, you should try to avoid using them, at least when outputting items in the frontend. When saving in the backend it is less problematic and you can use them here.

I know sometimes it's not easy because you don't know the ID of the category, or they are created dynamically and the IDs aren't exists yet. The best way to avoid slowness is to use a routing category with a single item that lists the routes to your categories/items, based on the pattern: known value => ID. It's a good practice to load this routes into memory at the beginning of the execution of your script and then map your categories and items based on the HTTP request:

Request Routing key Value
$_GET['products'] products array(2, 7, 12)

You then update the Routing Index every time a new item or category is created or updated in the backend. You can save the value as json or other format in longtext field, for example.