Replacing switch statements with polymorphism in PHP

Published by at 3rd October 2018 10:07 pm

For the last few months, I've been making a point of picking up on certain antipatterns, and ways to avoid or remove them. One I've seen a lot recently is unnecessary large switch-case or if-else statements. For instance, here is a simplified example of one of these, which renders links to different objects:

1<?php
2
3switch ($item->getType()) {
4 case 'audio':
5 $media = new stdClass;
6 $media->type = 'audio';
7 $media->duration = $item->getLength();
8 $media->name = $item->getName();
9 $media->url = $item->getUrl();
10 case 'video':
11 $media = new stdClass;
12 $media->type = 'video';
13 $media->duration = $item->getVideoLength();
14 $media->name = $item->getTitle();
15 $media->url = $item->getUrl();
16}
17return '<a href="'.$media->url.'" class="'.$media->type.'" data-duration="'.$media->duration.'">'.$media->name.'</a>';

There are a number of problems with this, most notably the fact that it's doing a lot of work to try and create a new set of objects that behave consistently. Instead, your objects should be polymorphic - in other words, you should be able to treat the original objects the same.

While strictly speaking you don't need one, it's a good idea to create an interface that defines the required methods. That way, you can have those objects implement that interface, and be certain that they have all the required methods:

1<?php
2
3namespace App\Contracts;
4
5interface MediaItem
6{
7 public function getLength(): int;
8
9 public function getName(): string;
10
11 public function getType(): string;
12
13 public function getUrl(): string;
14}

Then, you need to implement that interface in your objects. It doesn't matter if the implementations are different, as long as the methods exist. That way, objects can define how they return a particular value, which is simpler and more logical than defining it in a large switch-case statement elsewhere. It also helps to prevent duplication. Here's what the audio object might look like:

1<?php
2
3namespace App\Models;
4
5use App\Contracts\MediaItem;
6
7class Audio implements MediaItem
8{
9 public function getLength(): int
10 {
11 return $this->length;
12 }
13
14 public function getName(): string
15 {
16 return $this->name;
17 }
18
19 public function getType(): string
20 {
21 return $this->type;
22 }
23
24 public function getUrl(): string
25 {
26 return $this->url;
27 }
28}

And here's a similar example of the video object:

1<?php
2
3namespace App\Models;
4
5use App\Contracts\MediaItem;
6
7class Video implements MediaItem
8{
9 public function getLength(): int
10 {
11 return $this->getVideoLength();
12 }
13
14 public function getName(): string
15 {
16 return $this->getTitle();
17 }
18
19 public function getType(): string
20 {
21 return $this->type;
22 }
23
24 public function getUrl(): string
25 {
26 return $this->url;
27 }
28}

With that done, the code to render the links can be greatly simplified:

1<?php
2return '<a href="'.$item->getUrl().'" class="'.$item->getType().'" data-duration="'.$item->getLength().'">'.$media->getName().'</a>';

Because we can use the exact same methods and get consistent responses, yet also allow for the different implementations within the objects, this approach allows for much more elegant and readable code. Different objects can be treated in the same way without the need for writing extensive if or switch statements.

I haven't had the occasion to do so, but in theory this approach is applicable in other languages, such as Javascript or Python (although these languages don't have the concept of interfaces). Since discovering the switch statement antipattern and how to replace it with polymorphism, I've been able to remove a lot of overly complex code.