General developer forum

[RFC] has_capability() and load_user_capability() across contexts

Martin Langhoff - Sailing
[RFC] has_capability() and load_user_capability() across contexts

The current has_capability() and load_user_capability() are very focused on the logged in user and get the caching mostly right so that stuff happens reasonably fast.

However, we need a separate set of functions to walk contexts -- functions that

  • Don't call `load_user_capability()"
  • Don't try and invoke the enrolment plugins!
  • Can be cached across checks, but with a different caching scheme from the one for users...

There are right now 2 scenarios I can see where the current setup is doing a lot of unnecessary work, and I can't see any reasonable fix. There is a ton of work to do here so that we can say something along the lines of

    // pre-walk the data we'll need
    // for example, showing participants
    // or forum mailouts
    foreach (users as user) {


    // pre-walk the data we'll need
    // for example, showing the courses
    // for user account
    // similar to get_my_courses() but does
    // not use the infrastructure that assumes
    // that the user is logged in and instead 
    // uses a a different cache
    $courses = get_user_courses();
    foreach (courses as course) {

Right now we depend on has_capability() for in all and every scenario. And has_capability() has ended up doing a lot of caching and magic behind the scenes -- that benefits the single-user-logged-in-now scenario, but make things hard to deal with whenever we are working across contexts.

This affects the following scenarios:

  • Login as admin, and visit a user profile with several enrolments. For every enrolment, there's check for has_capability() -- the problem is that it ends up calling load_user_capability() which in turn calls check_enrolment_plugins(). If you are using any external enrolment plugin, this is a mess.
  • As admin, browsing a participants list in "detailed view" mode triggers the same codepath.

And the trick in those scenarios is that we are getting a ton of data that we cannot cache efficiently.

Separating these functions from has_capability() also means that we can drop odd bits and pieces from has_capability(), and focus it on the currently-logged-in-user case.

Average of ratings: -
Martin Langhoff - Sailing
Re: [RFC] has_capability() and load_user_capability() across contexts

One quick comment here - I think we can make this scheme working very efficiently by just adding a "path" field to the context table.

Having context path listing the "parent" contexts (identical to course_categories.path but tracking context ids) means that we can very quickly

  • For a deep-down nested context, get the whole hierarchy of relevant contexts in 1 query, and all the relevant role-assignments with an additional query using IN() - or in a single query with a JOIN.
  • If we are looking at a course page, we can in 1 query look up all the contexts we are going to need in that page saying WHERE path LIKE '$coursecontextpath/%'.

So roles-and-capabilities can be resolved in a fixed number of queries. Which means that

  • we won't need caches as much
  • contexts become more useful, and at the same time we can just JOIN through them rather than have to load them into a clumsy PHP object
  • where we need a cache, we can populate it with a single query - rather than per-entry as we do now
  • as the granularity is better defined, it makes more sense to share those caches via memcached or mmcache

The downside is... maintaining the path when courses or categories move. But we were just discussing this before, and in 1.7/1.8 this has to be an expensive operation anyway as has lots of repercussions enrolment-wise. (It's not handled correctly right now, but that needs to be fixed anyway...)

Average of ratings: -
Martin Dougiamas
Re: [RFC] has_capability() and load_user_capability() across contexts
Core developersDocumentation writersMoodle HQPlugin developersTesters
Yeah, I can see that ... messy.

load_user_capability should definitely not be checking all the enrolment plugins when it is being called from has_capability. That could be pretty easy to fix with a flag.

Average of ratings: -