Is there a way to dynamically assign an API version to a client based on their client app version?

I’m currently using URI versioning.

My index.php file looks something like this:

#declare middleware
$mw_getVersions = //...(extract client app version from header, and api version from URI)
$mw_devAuth = //...
$mw_prodAuth = //...

#set routing and error middleware
$app->addRoutingMiddleware();
$errorMw = $app->addErrorMiddleware(false, false, false);
$errorMw->setDefaultErrorHandler(//...);

#route
$app->group('/api', function($app) use($mw_getVersions, $mw_devAuth, $mw_prodAuth){
    //dev api for testing
    $app->group('/dev', function($app){
        foreach(getAPIFiles('dev') as $file)
            include_once($file);
    })->add($mw_devAuth);

    //production
    $app->group('', function($app){
        //v1.0
        $app->group('/v1.0', function($app){
            foreach(getAPIFiles('v1.0') as $file)
                include_once($file);
        });

        //v1.1
        $app->group('/v1.1', function($app){
            foreach(getAPIFiles('v1.1') as $file)
                include_once($file);
        });
    })->add($mw_prodAuth);

})->add($mw_getVersions);

#run app
$app->run();

where getAPIFiles($version) returns an array of file paths for the correct api, like ['src/v1.0/controllers.php', 'src/v1.0/datasources.php'].

I don’t know if this is good practice or not, but it made sense to me so I stuck with it. This means that the client application (in my case a mobile app) needs the api endpoint embedded in its source code. The problem I’m foreseeing with this method is that I don’t have a highly responsive way to control which api the client uses without messing up something down the line.

Is there a way (and would it be considered good practice) to dynamically assign an API version (or at least a set of included files) at runtime, based on the client app version?
For example, by mapping client versions to api version:

$client_to_api_map = ['1.2.5' => 'v1.1',
                      '1.0.0' => 'v1.0'];

so any client with version in range of 1.0.0-1.2.4 will be directed to api v1.0, and anything with 1.2.5 or above would use the new v1.1 api. The problem with this dynamic approach is that I cannot find a way to inject a variable inside the route, like so:

$app->group('/v' . mapClientToApi($request->getAttribute('client_version')), function($app){
    foreach(getAPIFiles('/v' . mapClientToApi($request->getAttribute('client_version'))) as $file)
        include_once($file);
});

From my understanding, the route is resolved before my $mw_getVersions middleware has a chance to extract the client version.

Any input or feedback is highly appreciated! Thanks!!

I would recommend moving this logic for the versions into a Middleware. Then you have better control because you can use the request and response object, and you can control the time when to extract the version from the request.

@odan I’m not too sure what you mean. I already extract the client version in a middleware (application, not route). I just don’t know how to use it to route a user to using a specific set of files.

Are you suggesting include-ing my files (including my routes.php file) inside my application middleware? I think I actually used to do that, but it was causing problems if I recall correctly.

Basically my question is this: how do I route users (to a completely different set of files) if I want to stop using URI versioning?

Edit:

It seems that something like what was suggested in this post might help me. Basically use $_SERVER[...] to extract the $value I need (be it URI /v1.0 version, the Accept: header, or a custom Client-App-Version header), switch($value) and include files as needed.
Does this sound like a good solution/practice?

Thanks for clarification. Now I see that you already implemented the version number logic in a middleware. As mentioned, I only mean the logic to extract the version, but not the logic to load other files.

First there are some things to consider, for example a dot “.” in a route is interpreted as a regex and may not work in Slim, even if you define a route like $app->get('/v1.0.0', ...) and so on. It will give you an 404 not found error. This applies to routing groups and routing placeholder as well. So maybe consider to “simplify” the version URL schema by using a full numbers version, like /v1, /v2, etc. With this API version concept, you could also simplify the routing with just some routing groups.

The other point (question) is how to “include” other files. I would not recommend to manually include PHP files. Instead I would implement classes that will be loaded by the composer autoloader.

The suggested post makes no sense to me, because it does not use Slim for the routing and implements its own router.

My goal here is to stop using this approach altogether (URI versioning). Instead, I would like to dynamically route my client to a certain set of files/namespaces based on a custom header they provided, such as their current client app version. Which leads me to your suggestion:

Could you kindly give me some pointers on how to do that? I honestly have been avoiding using the autoloader for my own code because I don’t fully understand it. I understand that it basically includes all files in my “composer” directory so I can reference any namespace/class from anywhere in my code.

Thanks again, I highly appreciate all the help :smiley:

For libraries that specify autoload information, Composer generates a vendor/autoload.php file. You can include this file and start using the classes that those libraries provide without any extra work:

I would guess, that somewhere in your Slim project, you have a line like this to load the Autoloader.

require_once __DIR__ . '/../vendor/autoload.php';

This is needed for Slim and also for your application specific classes.

You can even add your own code to the autoloader by adding an autoload field to composer.json.

"autoload": {
    "psr-4": {
        "App\\": "src/"
    }
},

Composer will register a PSR-4 autoloader for the App namespace.

You define a mapping from namespaces to directories. The src/ directory would be in your project root, on the same level as the vendor directory. An example filename would be src/Foo.php containing an App\Foo class.

Thanks! Took all day to refactor my code to work with autoload and namespaces, but finally got it working :smiley: I looked through your Slim skeleton project to get a sense on how to best structure files and directories, but the approach is really different than what the Slim docs used, so I’m finding it hard to keep track of the flow of the program. For instance, I was looking at your CustomerCreatorAction and I noticed that you never manually instantiate it, but I also don’t see you adding it to your DIContainer in config/container.php – so when does it get created?? I feel like I’m missing something big :laughing:

Anyway, back to topic: so say I do the mapping like I mentioned earlier (app version to api version): where and how in my code do I direct a user to a certain namespace? That’s the part that’s not entirely clear to me.

Right now, my first route group ("/api") is the one that has a middleware that extracts the client_app_version. I just don’t know where and how to direct a user to mynamespace/v1 versus mynamespace/v2 based on that client_app_version. Where am I putting a switch statement?

Hey! :slight_smile:

Haven’t heard back in a couple of days. Do you think you can take one more look at my last response? I feel like I’m really close, just missing something small :smiley:
Thank you so much!

You can find more information about the automatic object creation here: Autowiring.

You could organize your namespaces, for example by API version.

  • App\Action\Version1
  • App\Action\Version2

Under each of these namespaces, you can put all Action classes that belongs to that version of the API. Then add a route group for each version and reference the version specific Action classes for each route. So you don’t need any switch or manually loading of files. Slim does only support routing on URL path level, so I can only recommend this way. Routing without URL path is not supported or must be implemented manually.

use Slim\App;
use Slim\Routing\RouteCollectorProxy;

// Version 1
$app->group(
    '/api/v1',
    function (RouteCollectorProxy $app) {
        $app->get('/customers', \App\Action\Version1\Customer\CustomerFinderAction::class);
        $app->post('/customers', \App\Action\Version1\Customer\CustomerCreatorAction::class);
    }
);

// Version 2
$app->group(
    '/api/v2',
    function (RouteCollectorProxy $app) {
        $app->get('/customers', \App\Action\Version2\Customer\CustomerFinderAction::class);
        $app->post('/customers', \App\Action\Version2\Customer\CustomerCreatorAction::class);
    }
);