Explore Your Types
Don’t worry, this isn’t a ‘what type of sandwich are you?’ kinda post. Instead we’ll look at how we can safely add types to our legacy code.
Adding types
There are really two ways of adding types. You either declare what you want your types to be, or you
declare what the types can be. When writing new code we should always be precise in our types, so we declare
what we want. We can say that a property is a string, or that a method only takes a User
class as a parameter.
But for our legacy code, this is a bit different. Lets take, for example this snippet from a Login
class i
recently had to deal with. While password is annotated as a string, when we look at the implementation, we see
that false is also an option.
class Login
{
/**
* @var string
*/
private $password;
public function login($email, $password = false)
{
// stuff
$this->password = $password;
}
}
Tools like Psalm and PHPStan will tell us there is an error here, or you could find it on your own. This is a
problem we’d like to solve, but there are multiple ways of doing so.
One would be to add a (string)
cast, before the assignment of $password
. The problem with that, is that we
are changing behaviour of the code. Another place may have a check like $this->password === false
.
The best solution would be to annotate password with string|false
. And if later it turns out it can also be a
resource, pointint to a private key, then we turn it into string|false|resource
. Now, if it can also be a Password
object, then we make it string|false|resource|Password
. We must however, resist just ploping down
mixed
and calling it a day.
By using mixed
we might as well have not added any types. Instead, we want to document our types as precise as
possible, wherever we can. Now, the example of the Login class is small,
but when you have a class with 200 methods, all calling each other and using its output, finding out what type something is can be difficult. You have to start with a type you are sure of. And if you can, make it an explicit type, in the language.
For example, what if we have the following method in a legacy class. When we read the code, we still don’t
know what $orderData
can be. Maybe the fetchOrder
method will hint us to that. But when we read it we can
guarantee that it returns an int
or false
. We can add it right away, and know we won’t change any behaviour.
class Legacy
{
// 100 methods
private function getOrderId($orderData)
{
$orderId = $this->database->fetchOrder($orderData);
if (! $orderId) {
return false;
}
return (int) $orderId;
}
// 100 more methods
}
So now we’ve added the type to our method. We may need to update the baseline of our static analysis tool,
or maybe we don’t have to do any extra work. Now we can be sure that when we use the getOrderId
method, we
get either an integer, or false. This (hopefully) means that we can add types elsewhere.
class Legacy
{
// 100 methods
private function getOrderId($orderData): int|false
{
$orderId = $this->database->fetchOrder($orderData);
if (! $orderId) {
return false;
}
return (int) $orderId;
}
// 100 more methods
}
So whats the point
The first point of adding these types is to increase our understanding of what this code does. By giving it the proper types, we may detect dead code, or find bugs. If we keep up this, and the boyscout rule long enough, then it may not be legacy code anymore.
The second point is that your ’new’ or better code probably still has to connect with legacy code. So by adding a bit more certainty, we are a bit safer that the code does what we expect it to do.
So go out there, and explore all the types your codebase has. So what if the parameter can be one of 12 interfaces, a string, or a boolean. Only once the horrors are documented do you have a chance to improve them.