Shuffling combined with Pagination

Hi,

I have a collection of pages, which I have generated in a controller. I am using pagination on this collection.

I now need to shuffle the pages, so they appear in a random order, while also maintaining the pagination. I have tried shuffle(), but this re-shuffles on each pagination so isn’t working as I need. I need to shuffle all the pages, and then have the pagination step through that shuffled collection without re-shuffling.

I found a plugin on here using a seed, but it doesn’t seem to be working for me. If anyone has any thoughts that would be great. My controller code is below for reference (with my current shuffle() near the end):

/site/controllers/art-and-artists.php

<?php

return function($page,$site,$kirby,$pages) {

$shared = $kirby->controller('site' , compact('page', 'pages', 'site', 'kirby'));

// fetch the basic set of pages
$artworks = $site->find('art-and-artists')->children()->sortBy('lastName','asc')->children()->published();

// fetch the set of artwork categories used in all artworks
$categories = $artworks->pluck('categories', ',', true);
// this is the categories defined for use on the page
$pageCategories = $page->artworkCategories()->toStructure();

$categoriesForFilter = [];

foreach($pageCategories as $category):
  $slug = $category->category()->slug();
  if(A::has($categories, $slug)):
    $categoriesForFilter[] = $category->category();
  endif;
endforeach;


// fetch the artists
$artists = $site->find('art-and-artists')->children()->sortBy('lastName','asc')->published();


// now for some filtering
$artistFilter      = get('artist');
$categoryFilter    = get('category');
$priceFilter       = get('price');

$artworks = $artworks
  ->when($categoryFilter, function ($categoryFilter) {
    return $this->filterBy('categories', $categoryFilter, ',');
  })
  ->when($priceFilter, function ($priceFilter) {
    return $this->filterBy('priceBracket', $priceFilter, '|');
  })
  ->when($artistFilter, function($artistFilter) {
    return $this->filter(fn ($child) => $child->parent()->slug() == $artistFilter);
  });

// apply pagination
$artworks   = $artworks->shuffle()->paginate(48);
$pagination = $artworks->pagination();

return A::merge($shared , compact('artworks','artists','categoriesForFilter','pagination'));

};

?>

I think the issue is that you are chainigng paginate on to shuffle. Maybe try:

$artworks = $artworks
  ->when($categoryFilter, function ($categoryFilter) {
    return $this->filterBy('categories', $categoryFilter, ',');
  })
  ->when($priceFilter, function ($priceFilter) {
    return $this->filterBy('priceBracket', $priceFilter, '|');
  })
  ->when($artistFilter, function($artistFilter) {
    return $this->filter(fn ($child) => $child->parent()->slug() == $artistFilter);
  })->shuffle();

// apply pagination
$artworks   = $artworks->paginate(48);
$pagination = $artworks->pagination();

I guessed at that, hopefully it works :slight_smile:

That’s true, but @mikeharrison also writes that he wants them to be shuffled, but then that the shuffled order persists within the paginated pages.

The issue here is that once you click on page 2 or so of your pagination, that’s a whole new request and Kirby has no idea about the result of the shuffling of your first request. So it will shuffle new, paginate that newly shuffled collection and get you page 2 of that.

For what you, @mikeharrison, want, you’ll need to store the result of the first shuffling in some sort of cache and get the collection from that cache in subsequent requests.

So would a collection work? Collections | Kirby CMS

Or would it reshuffle in the pagination still each time you hit the collection? Sorry, i dont do alot of blog post / paginated product stuff… its an area of kirby i dont have much first had experience of.

No it wouldn’t just out of the box. Because page 1 will be a totally separate request from page 2 – and there is no way Kirby knows how to shuffle the pages, but shuffle them in the same way between these two requests, if

Another way instead of trying to store the order of the shuffled collection could be to actually update each artwork page with a random number in a field, e.g. mySort, and then to sort by that field. This would be consistent then across requests.

Could be combined with a cronjob that overwrites the field for each artwork with a new random number. So the artworks are shuffled ever x hours/days…

That sounds a bit crazy :slight_smile:

Why?

It’s an uncommon request to shuffle and paginate a collection at the same time. But how would you implement it otherwise?

:person_shrugging: Just seems mad to have to set up a cron job and all… i get the technacalities of the problem.

Well, to be frank, not really helpful when trying to find a solution for @mikeharrison.

Woops… hey look i didnt mean to derail this. Im genuinely trying to help here and explore solutions.

I have not tested it because the prev/next navigation on my blog is more complex. Therefore, I cannot guarantee its functionality. But perhaps the explanations provide a solution that can be adopted. And maybe the code can be compressed if it is easier to do so.

<?php

// Function to shuffle the blog's children pages and cache the result
function shuffledChildren($page) {
    // Key for the cache
    $cacheKey = 'shuffled_blog_' . $page->id();
    
    // Cache instance
    $cache = kirby()->cache('pages');
    
    // Check if a cached version already exists
    if ($cached = $cache->get($cacheKey)) {
        // Return already cached data
        return $cached;
    } else {
        // Shuffle the pages randomly
        $children = $page->children()->listed()->shuffle();
        
        // Store the cached data and set the expiration to 24 hours
        $cache->set($cacheKey, $children, 1440); // 1440 minutes = 24 hours
        
        return $children;
    }
}

// Retrieve the shuffled child pages
$children = shuffledChildren($page);

// Previous and next page based on the shuffled order
$index = $children->indexOf($page);
$prevPage = $children->nth($index - 1);
$nextPage = $children->nth($index + 1);
?>

<!-- Navigation -->
<?php if ($prevPage): ?>
    <a href="<?= $prevPage->url() ?>">previous page</a>
<?php endif ?>

<?php if ($nextPage): ?>
    <a href="<?= $nextPage->url() ?>">next page</a>
<?php endif ?>

@mikeharrison Here the cached version solution is integrated into your code. I can’t test the functionality, for that I would have to know your entire project. But you are welcome to use the code and tell me if your question has been answered.

<?php

return function($page, $site, $kirby, $pages) {

    // Fetch the shared data from the site controller
    $shared = $kirby->controller('site', compact('page', 'pages', 'site', 'kirby'));

    // Fetch the basic set of artworks
    $artworks = $site->find('art-and-artists')->children()->listed()->sortBy('lastName', 'asc')->children()->published();

    // Fetch all unique artwork categories used in the artworks.
    $categories = $artworks->pluck('categories', ',', true);

    // Categories defined specifically for use on the current page
    $pageCategories = $page->artworkCategories()->toStructure();
    $categoriesForFilter = [];

    foreach ($pageCategories as $category) {
        $slug = $category->category()->slug();
        if (A::has($categories, $slug)) {
            $categoriesForFilter[] = $category->category();
        }
    }

    // Fetch artists, sorted by their last name and only showing published ones
    $artists = $site->find('art-and-artists')->children()->listed()->sortBy('lastName', 'asc')->published();

    // Apply filters based on query string parameters (artist, category, price)
    $artistFilter = get('artist');
    $categoryFilter = get('category');
    $priceFilter = get('price');

    $artworks = $artworks
        ->when($categoryFilter, function ($categoryFilter) {
            return $this->filterBy('categories', $categoryFilter, ',');
        })
        ->when($priceFilter, function ($priceFilter) {
            return $this->filterBy('priceBracket', $priceFilter, '|');
        })
        ->when($artistFilter, function ($artistFilter) {
            return $this->filter(fn($child) => $child->parent()->slug() === $artistFilter);
        });

    // Shuffle the artworks once per day and cache the result using Kirby's caching system
    $artworks = getCachedShuffledArtworks($artworks, $page);

    // Apply pagination (48 artworks per page)
    $artworks = $artworks->paginate(48);
    $pagination = $artworks->pagination();

    // Return all variables, including shared data and filtered results
    return A::merge($shared, compact('artworks', 'artists', 'categoriesForFilter', 'pagination'));
};

// Function to shuffle artworks and cache the result for 24 hours
function getCachedShuffledArtworks($artworks, $page) {
    // Create a unique cache key based on the page ID
    $cacheKey = 'shuffled_artworks_' . $page->id();

    // Get the cache instance for pages
    $cache = kirby()->cache('pages');

    // Check if there's a cached version of the shuffled artworks
    if ($cached = $cache->get($cacheKey)) {
        return $cached;
    } else {
        // Shuffle the artworks and cache the result for 24 hours
        $shuffledArtworks = $artworks->shuffle();
        // Define the cache duration as 1440 minutes (24 hours)
        $cache->set($cacheKey, $shuffledArtworks, 1440); // 1440 minutes = 24 hours
        return $shuffledArtworks;
    }
}

@distantnative @jimbobrjames :see_no_evil: Please don’t stone me if I go completely wrong with my solution and cause a kernel panic.

Hi!

Thanks everyone for your thoughts. For context this is for an art auctioneer, who wants to display their whole catalogue (hence the pagination), but not always show the same artists on the first page (hence the shuffle).

I have ended up with the solution below, which is influenced by another post on this forum:

/site/plugins/shuffleSeed/index.php

<?php

Kirby::plugin('mhd/shuffleSeed', [
    'pagesMethods' => [
        'shuffleSeed' => function($seed = null) {

            if(is_null($seed)) {
                $seed = kirby()->session()->get('seed');
            
                if(is_null($seed)) {
                  $seed = time();
                  kirby()->session()->set('seed', $seed);
                }
            }

            mt_srand($seed);

            $keys = array_keys($this->data);
            $size = count($keys);

            $order = array_map(function($val) {return mt_rand(); }, range(1, $size));
            array_multisort($order, $keys);

            $collection = clone $this;
            $collection->data = [];

            foreach ($keys as $key) {
                $collection->data[$key] = $this->data[$key];
            }

            return $collection;
        }
    ]
]);
?>

/site/controllers/art-and-artists.php

<?php

return function($page,$site,$kirby,$pages) {

$shared = $kirby->controller('site' , compact('page', 'pages', 'site', 'kirby'));

// fetch the basic set of pages
$artworks = $site->find('art-and-artists')->children()->sortBy('lastName','asc')->children()->published();

// fetch the set of artwork categories used in all artworks
$categories = $artworks->pluck('categories', ',', true);
// this is the categories defined for use on the page
$pageCategories = $page->artworkCategories()->toStructure();

$categoriesForFilter = [];

foreach($pageCategories as $category):
  $slug = $category->category()->slug();
  if(A::has($categories, $slug)):
    $categoriesForFilter[] = $category->category();
  endif;
endforeach;


// fetch the artists
$artists = $site->find('art-and-artists')->children()->sortBy('lastName','asc')->published();


// now for some filtering
$artistFilter      = get('artist');
$categoryFilter    = get('category');
$priceFilter       = get('price');

$artworks = $artworks
  ->when($categoryFilter, function ($categoryFilter) {
    return $this->filterBy('categories', $categoryFilter, ',');
  })
  ->when($priceFilter, function ($priceFilter) {
    return $this->filterBy('priceBracket', $priceFilter, '|');
  })
  ->when($artistFilter, function($artistFilter) {
    return $this->filter(fn ($child) => $child->parent()->slug() == $artistFilter);
  });

// apply pagination
$artworks   = $artworks->shuffleSeed()->paginate(48);
$pagination = $artworks->pagination();

return A::merge($shared , compact('artworks','artists','categoriesForFilter','pagination'));

};

?>

This uses the session to store the seed for the shuffle, so clears it periodically. I think this is working as I need it to, though please shout if it looks like I can do this better / there is a problem with this solution that I haven’t seen!