bwrigley's avatar

Best practise for gathering a collection of child models for pagination

Still very new to Laravel and working on my first-ever project and I have some best practice questions about the architecture of a flashcards app that I'm building.

I have two models topic and flashcard both with corresponding controllers.

topic has a one-to-many relationship to child topics : $topic->children (which allows for a multi-level hierarchy).

topic also has a one-to-many relationship with flashcard : $topic->flashcards.

topic also has a nullable one-to-one relationship with a parent topic : $topic->parent

flashcard has a non-nullable relationship with a topic : $flashcard->topic

a topic can either have a parent-child relationship with topic or with flashcard but not both.

When the user is navigating through topics I want them to be able to click a 'test me' button at any point.

This will collect all of the current topic's flashcards OR all of the flashcards of all of the topics down the hierarchy.

I will then show these flashcards to the user one at a time and test the user.

I will then increment a 'correct' or 'false' value on the flashcard model, probably through a form submission, and then move on to the next card.

I have a couple of questions about how best to implement this testing part:

  1. To collect all of the flashcards belonging to all topics down the hierarchy, should I use eloquent so that I can paginate the forms one by one or build a Collection of flashcards and paginate myself in some way? If it's the latter, are there good ways to do this and keep the Collection in the session?

  2. And from a best practice POV, is it better design to have a recursive method in my TopicController that collects all the flashcards from all subtopics? This "feels" wrong to me, is there a better place to store this method?

Thanks for your advice!

0 likes
8 replies
webrobert's avatar

I think generally you are right about question 2 but not always. Sometimes depending on the page you are rendering it does make more sense to do some work there. I try to limit the amount of data I am looping and how many times I loop. if you pull a collection then map it then filter it then ... you get the idea. each time is a pass on the data. This is just my OCD. I think there is probably not much impact for most things. its just my personal best practice to think about how many rounds I make in all my queries and then the collection.

I don't quite follow your data but its seems like you could just do

FlashCard::whereRelation('topics', 'id', $theTopicId)  // the rest of the query or ->get() etc.
// Or
FlashCard::whereHas('topics', fn ($q) => $q->whereIn('id', $arrayOfTopicIds) )

Im sure there are plenty of search results for handling the presentation of the questions so I'll leave that bit out.

1 like
bwrigley's avatar

@webrobert thanks! Yes I've gone for using whereIn() now so that I can still use the paginate method:

class Flashcard extends Model
{
	///
    public function scopeFromTopics($query,$topics)
    {

        return $query->wherein('topic_id', $topics);

    }
}

and then calling from my TopicController (which still feels wrong, but it works)

class TopicController extends Controller
{
 ///
    public function runTest(Topic $topic)
    {

        $topics = $this->_getDescendants($topic,[]);  //recursive method

        $flashcards = Flashcard::fromTopics($topics)->paginate(1));

        return view('flashcards.test', [
            'flashcards' => $flashcards,
            'topic' => $topic
        ]);
    }
}
webrobert's avatar

@bwrigley

this method just goes thru to get the children ids?

$topics = $this->_getDescendants($topic,[]);  //recursive method

which is $topic->children ? If so...

public function runTest(Topic $topic)
{
    $flashcards = Flashcard::query()
        ->fromTopics([ $topic->id, ...$topic->children->map->id ])
        ->paginate(1);

    return view('flashcards.test', [
        'flashcards' => $flashcards,
        'topic' => $topic
    ]);
}
webrobert's avatar

Ahh wait, you have more than one level of children?

bwrigley's avatar

@webrobert so my topics can have subtopics which can have subtopics etc.

e.g. History might have both Modern History and Ancient History. Then Modern History might have WW1, WW2, American history etc and Ancient History might have Roman, Greek, Egyptian etc.

The user might navigate to Ancient History -> Greek and click 'Test me' and only see flashcards from Greek history. But if they navigate back up to the grand parent, History, and click 'Test me' they will be tested on all flashcards from all children, grandchildren, great-grand...

So I think I have to write some sort of recursive method, unless Eloquent has a secret helper or there is a more efficient SQL way to write this?

Thanks again!

webrobert's avatar
Level 51

@bwrigley

I understand now. You're right its not an ideal situation. But there are plenty of people who have used infinite category children - so there are lots of code ideas for it. And others limit the children (like here only allows for one level of reply in comments).

well I guess you could cache the topicIds from the recursion. If it got to be too much...

$topics = Cache::remember("topicId-{$topic->id}-withChildren", $seconds, fn () =>
     $this->_getDescendants($topic,[])
);

$flashcards = Flashcard::fromTopics($topics)->paginate(1));

return view('flashcards.test', [
  'flashcards' => $flashcards,
  'topic' => $topic
]);

https://laravel.com/docs/9.x/cache#retrieve-store

1 like
bwrigley's avatar

@webrobert yes I did the caching too. Thanks for your help and advice!

Actually in the end I've created a model called Test which has a many-to-many relationship with Flashcard. When the user hits 'Test Me' a new Test instance is created and all the child flashcards are collected, randomised, and attached to the new test.

At least this way the recursion only happens once.

Thanks again!

1 like

Please or to participate in this conversation.