The Slim Framework support forum has moved to http://discourse.slimframework.com. This Tender forum is no longer maintained or monitored.

Lazy Loading

David Rodger's Avatar

David Rodger

29 Oct, 2012 10:34 AM

I played around with Slim a little last year and enjoyed it. Now I'm back and enjoying 2.0. Thanks to Josh and the team for all the hard work.

Lately, I've been looking at Andrew's Share My Ideas app. It's simple and interesting. Yet, one thing occurs to me: both the login and ideas controllers must be created for every request regardless of whether or not they're being used. That's not a criticism; as Andrew says, it's a demo of various features of Slim and as far as I can see doesn't aim or claim to be a paragon of efficiency.

Anyway, I've been thinking about this and have come up with a means of lazy-loading and "on-demand" instantiation of classes and I'd like to share my idea if I may.

Slim is quite elegant but requires that, if object methods are to be used as callbacks, the objects must be instantiated at the time the associated route is defined. Which is, I suppose, the reason that Andrew did what he did.

I'll start by explaining that I like Andrew's use of a "wrapper" or perhaps a "decorator" class around the Slim app instance, so i copied that! The application class has a setup method in which the routes are defined.

My solution is to use a "dummy" callback as a placeholder and use a route middleware function (or method) which swaps the dummy callback for the intended one. This is possible because the Route is passed to the middleware; one simply retrieves the "dummy" callback to call to Route:getCallable() to injects the intended callback with Route:setCallable().

At the moment, the intended one is determined by the first segment of the resoureUri. Therefore, the uri "/user/:id" might result in the instantiation of a "UserController". The method called is determined by the "dummy" callback. When the swap is made, the dummy is called and returns the name of the method.

The dummy method is a magic __call() as part of my Application.

class Application
{
    /* excerpt only */

    public function __call($calledName, $args = array())
    {
        return $calledName;
    }
    public function setup()
    {
        //Swap out array($this, '__call') for array($controller, 'methodname') based on first segment of path
        $mw = array($this, 'injectController'); 
        
        $this->app->get('/', $mw, array($this, 'index'))->name('home');
        $this->app->map('/login', $mw, array($this, 'login'))->via('GET', 'POST')->name('login');
        $this->app->get('/logout', $mw, array($this, 'logout'))->name('logout');
    }

    public function injectController(\Slim\Route $route)
    {
        $classname = '\\Platform\\Controller\\'.$this->findControllerClass(); //double-slash namespace when used in a string
        $controller = new $classname;
        $controller->setApplication($this);
        $methodname = call_user_func($route->getCallable());
        $callable = array($controller, $methodname);
        $route->setCallable($callable);
    }
}

The method findControllerClass() determines which controller to use; there is a mechanism for injecting aliases; therefore, the LoginController can be used for '/login' and '/logout'.

I won't pretend this is the bests solution to the problem, especially as I'm not a developer by trade. But I'd be grateful to read what you think or learn of better solutions.

Regards, David

  1. Support Staff 1 Posted by Brian Nesbitt on 31 Oct, 2012 02:59 AM

    Brian Nesbitt's Avatar

    The general concept was discussed here:
    http://help.slimframework.com/discussions/questions/238-route-callb...

    Really how many routes do you have? A simple version as discussed before still uses closures for routes (or instantiated controller objects) but only includes (instantiates) on an as needed basis and goes something like this.

    $app = new Slim();
    
    $app->hook("slim.before.router",function() use ($app){
        if (strpos($app->request()->getPathInfo(), "/user") === 0) {
            require_once('user/routes.php');
        } elseif (strpos($app->request()->getPathInfo(), "/post") === 0) {
            require_once('post/routes.php');
        } elseif (strpos($app->request()->getPathInfo(), "/another") === 0) {
            require_once('another/routes.php');
        }
    });
    
    $app->run();
    

    Using a PHP opcode cache and depending on the complexity of findControllerClass() you will start to tip the scale back to even and maybe even the other way. I have done a little local benchmarking with a 30+ route project and it really made no difference.

    Not to mention, unless you are creating tons of routes and expensive objects the "slowest" part of Slim is the regular expression matching on the route expression and arguments. There are plans to greatly optimize this in the near future.

  2. 2 Posted by David Rodger on 31 Oct, 2012 03:43 AM

    David Rodger's Avatar

    I plan to have quite a few routes.

    It was the lazy loading (instantiation) of controller objects, not routes, that motivated me. When I had previously placed different routes in different files, I had trouble redirecting to routes that weren't loaded. For now, I've placed all routes in the setup() method, but I can revisit that of course.Your excerpt is a good way of reducing the number of routes and, since it occurs before the router, overcomes that issue.

    fimdControllerClass() is quite simple. It does what your strpos() would do and composes the classname from that.

    Thanks for your thoughts.

  3. 3 Posted by David Rodger on 01 Nov, 2012 05:43 AM

    David Rodger's Avatar

    OK. Changed things quite a bit.

    Thanks to this and Brian's comment above, I've placed routes in a setUp() method in controllers.

    The Application class now just instantiates the right controller based on the first part of PATH_INFO.

    This seems a lot cleaner, not least because it involves no "magic" methods and fewer calls to call_user_func() or call_user_func_array().

    Of course, since only a limited number of routes now exists for a single request, the helper method urlFor() mostly doesn't work. But I figure this is no great loss because if one has to remember route names, one can just as easily compose URIs for links and redirects. I do wonder, though, whether a future version might find some way of separating route patterns from the routes themselves, so that urlFor() might be useable in cases where the corresponding \Slim\Route has not been created.

  4. Support Staff 4 Posted by Brian Nesbitt on 03 Nov, 2012 03:02 AM

    Brian Nesbitt's Avatar

    Sounds like changes for the better :-)

    Well the pattern and name are both necessary since its a lookup by name and the original route URI is created from the associated pattern (after substituting any variables).

    Yes this is also on my to do list. I had a previous PR that just got merged a few days ago for simplifying routing. The next step is to improve the logic for matching routes so that the first matched route is returned immediately, improving performance since the majority of requests would have fewer regex's ran. This should allow most sites to add all routes which would allow the use of urlFor() without as much of a performance hit.

  5. Support Staff 5 Posted by Brian Nesbitt on 05 Nov, 2012 01:30 PM

    Brian Nesbitt's Avatar

    Had some more thoughts on another implementation using Pimple. Summarized the idea here:
    http://nesbot.com/2012/11/5/lazy-loading-slim-controllers-using-pimple

Reply to this discussion

Internal reply

Formatting help / Preview (switch to plain text) No formatting (switch to Markdown)

Attaching KB article:

»

Attached Files

You can attach files up to 10MB

If you don't have an account yet, we need to confirm you're human and not a machine trying to post spam.

Keyboard shortcuts

Generic

? Show this help
ESC Blurs the current field

Comment Form

r Focus the comment reply box
^ + ↩ Submit the comment

You can use Command ⌘ instead of Control ^ on Mac