Dynamic API Versioning in Laravel
Tags: php, laravel, oop, tutorial
Published: 2024-01-31 16:06:08

**Recently, I applied for a software engineering job**, and at first, I was too excited about it. Yes, there were some tasks to do that finally made me give up because I didn't have much time to do all of what was wanted in the description, and I also had another interviews. So I didn't get through the tasks. Believe me or not, it was like an actual backlog!


![The task description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/idztaanbs1t5bhc67vz2.jpeg)

## The problem
But, one of the tasks impressed me to do some stuff and write this post. I tell you about the question and clarify the problem:
1. We build an API (v1).
2. Then we have another version (v2) with some new features.
3. Not every feature has changed, let's keep it [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself)!

```
Controllers/
├─ v1/
│  ├─ PostController.php
├─ v2/
│  ├─ PostController.php
```
Let's suppose that we don't have the `show()` method in the `v2/PostController.php` because we don't need it. But the first version's `index()` method has a totally different implementation than the second version. Consider that the boss doesn't like any duplication!

How do you implement it?

<hr>

## Solution #1: Inheritance
I assume that you know what [inheritance](https://en.wikipedia.org/wiki/Inheritance_(object-oriented_programming)) is and get directly into my first solution to the problem.

In the first version, we implement everything we need normally. When we start building the second version's endpoints, we just implement what's changed. I demonstrate the idea with this example:

**api.php**
```php
Route::get('/v1/posts', [V1PostController::class, 'index']);
Route::get('/v1/posts/{id}', [V1PostController::class, 'show']);

Route::get('/v2/posts', [V2PostController::class, 'show']);
Route::get('/v2/posts/{id}', [V2PostController::class, 'show']);
```

**V1/PostController.php**
```php
<?php

namespace App\Http\Controllers\API\V1;

use App\Http\Controllers\Controller;

class PostController extends Controller
{
    public function index()
    {
        return 'V1 index';
    }

    public function show($id)
    {
        return "V1 show: {$id}";
    }
}
```

**V2/PostController.php**
```php
<?php

namespace App\Http\Controllers\API\V2;

use App\Http\Controllers\API\V1\PostController as V1PostController;

class PostController extends V1PostController
{
    public function index()
    {
        return 'V2 index';
    }
}
```

As we haven't overridden the `show()` method, the second version's controller provides the first version's functionality and opening `https://localhost/api/v2/posts/1` shows us:

> V1 show: 1
  
<hr>

## Solution #2: handleVersion()
The problem with the previous solution is that we need to define all the routes, even if they do the same thing in the previous version. What's the point of defining the `/v2/posts/{id}` route if it's not actually implemented? Are we supposed to repeat a hundred routes per version? Of course not!

Another fix came into my head! How does router work? Let's take a glance:
```php
Route::get('uri here', ['controller namespace', 'method name']);
```

So I wrote such a function:
**helpers.php**
```php
function handleVersion(string $controller, string $method)
{
    $latestApiVersion = config('app.latest_api_version');
    preg_match("/api\/v(\\d+)/i", request()->path(), $apiVersion);
    $apiVersion = $apiVersion[1] ?? $latestApiVersion;

    if ($apiVersion > $latestApiVersion) {
        abort(404);
    }

    $namespace = "App\Http\Controllers\API\V{$apiVersion}\\{$controller}";

    while (! method_exists(app($namespace), $method)) {
        if ($apiVersion === 0) {
            abort(404);
        }

        $apiVersion--;
        $namespace = "App\Http\Controllers\API\V{$apiVersion}\\{$controller}";
    }

    return [$namespace, $method];
}
```

And used it as bellow:
**api.php**
```php
Route::get('{version}/posts', handleVersion('PostController', 'index'));
Route::get('{version}/posts/{id}', handleVersion('PostController', 'show'));
```

Now, I don't have to define plenty of pointless routes anymore!
  
<hr>

## Solution #3: A combination of my two solutions
I liked the idea of extending the previous version controllers; hence, I decided to combine the two solutions! If you're going to create empty controllers, you can get rid of the `while()` loop in the `handleVersion()`.
  
<hr>

## Last words!
I truly enjoyed facing this challenge, and this post is about two ideas I'd thought about. **If you have a better idea**, please **feel free to share it** with us in the comments.