A better way to reference things in your application
We constantly reference things within our application with numbers and strings, but is there a better way?
We, as programmers, use id's continuously throughout our day and probably would be lost without them. An id can be a simple integer that increases when a new record is inserted into a database. It can also be as complicated as a UUID which is more or less unique at the time of generation.
In the long run, id's have only one practical use: identifying things, and with things, I mean everything. An id can be used to represent a user in your database. A drive on your computer. Or a transaction from your bank. An id can represent god knows what resource.
Id's can identify everything around us, which's its strongest point and its greatest downfall. Do you know what id = 5
or UUID = 00145851-4612-4513-82ca-68b77df0a0b0
represents? Though the id can point to a specific resource, it does not tell you what kind of resource it is.
Stripe solves this by prepending the id's with a type it is representing. A customer id will always start with cus_
, and a charge always has ch_
prefixed. The id does not only identify a specific resource. It also tells you what kind of resource it is.
I'm in love with this concept. Every time you're working with id's within your application. You know the kind of resources you're working with. But wouldn't it be cool if your programming language and IDE also knows which resources these id's represent?
Let's take a look at a simple blogging application where a user can comment on a post. I'm using Laravel in this example, but you can use this method in every framework or programming language.
At some point you want to write a piece of code to add a new comment to a post, the signature of such piece of code may look like this:
public function addComment(
int $userId,
int $postId,
string $content
): Comment;
Now comes the tricky part, what if within a few months we decide to switch the order of the id's because it feels more natural:
public function addComment(
int $postId,
int $userId,
string $content
): Comment;
We'll need to update all these method calls throughout our codebase, so the code keeps working. What if we forget one? At some point, the id of a post will be given as user id and vice versa. These kind of errors are difficult to spot and take a lot of time debugging.
There are solutions to this problem, we could for example always use models as a parameter to the function:
public function addComment(
Post $post,
User $user,
string $content
): Comment;
When we provide a User
model as the first parameter, PHP will complain because the types are not the same. We get an exception and don't need to debug for hours to find the problem, neat!
But, we always need to have the Post
and User
models loaded when we want to call this method. Hydrating models can be quite resourceful, especially when you've got a lot of models. What if we want to represent id's not tied to models, such as the Stripe resources we saw earlier? We don't have models for each Stripe charge or customer, so we cannot use them as a type for our method.
Let's go back to where we started. The only thing we want is typing our id's so we can pass them to functions correctly. What if we put these id's, the strings and integers in their own object? It's a little bit of both worlds, we don't need models and can represent non-models, but we keep the strong typing:
class PostId implements Stringable{
public function __construct(
private int $id
){}
public function id(): int
{
return $this->id;
}
}
When we also create such id for the User
model we can now refactor out initial piece of code to this:
public function addComment(
PostId $postId,
UserId $userId,
string $content
): Comment;
Great, passing a UserId
as a PostId
will cause an exception! Since we'll have a lot of these id classes, let's create a base class for them:
abstract class Id implements Stringable{
public static function create(int $id): static
{
return new static($id);
}
public function __construct(
private int $id
){}
public function id(): int
{
return $this->id;
}
public function equals(int|Id $other): bool
{
return (string)$this === (string)$other;
}
public function __toString(): string
{
return $this->id;
}
}
Now we let PostId
and UserId
extend from this Id
class, which gives us the ability to create a new id and easily compare them.
Since these id's represent models, we can take this even a bit further in a Laravel application. Let's create yet another abstract class: ModelClass
:
abstract class ModelId extends Id{
abstract protected function getModelClass(): string;
public function __construct(
private int $id
){}
public function model(): Model
{
return $this->modelClass::find($this->id);
}
public function exists(): bool
{
return $this->model() !== null;
}
}
We let PostId
and UserId
extend from ModelId
and need to implement the abstract function getModelClass
:
class PostId implements Stringable{
public function getModelClass(): string
{
return Post::class;
}
}
Now we can do the following:
$postId = PostId::create(10);
When we can easily check if the model exists:
$postId->exists(); // true
Or get the model itself:
$postId->model(); // Post object
This is only useful for id's representing models. The Stripe id's we talked about in the beginning would only extend from the Id
class since they're not models.
We don't have to stop here! In our project, we've added some extra functionality to these abstract classes like:
- creating multiple id's in one go
- the ability to pass null to the create function, which will return null instead of an
Id
object - a simple helper to get the morph class from a
ModelId
Let's go full circle. We can easily make a new Id
with the create
method, but wouldn't it be cool if we could get the Id
from a model like this:
$post->getId();
We create a new interface which the Post
and User
models will implement:
interface WithId{
public function getId(): Id
}
Now we add this interafce to our models as such:
class Post extends Model implements WithId{
public function getId(): Id
{
return new PostId($this->id);
}
}
And we're done! As a bonus, since our Id
class is Stringable
, we can still use the Eloquent functions that Laravel provides, for example, this will work:
$postId = PostId::create(20);
$post = Post::findOrFail($postId); //Post object
When you're writing big applications, encapsulating id's can make your code a lot more readable for everyone working on the project. Give it a try in your next project and let me know what you think about it!