In essence, everything is a string.
Well, you can always go one layer deeper and find out what a string really is, but for web apps I work on, both input data and output data are strings. The input is an HTTP request, which is a plain-text message that gets passed to the web server, the PHP server, the framework, and finally a user-land controller. The output is an HTTP response, which is also a plain-text message that gets passed to the client. If my app needs the database to load or store some data, that data too is in its initial form a string. It needs to be deserialized into objects to do something and later be serialized into strings so we can store the results.
We create objects from strings, and turn them back into strings because the protocols we use require strings (e.g. HTTP, SQL, AMQP, and so on). These protocols are only used near the edges of the application, where data comes in from and gets sent to external systems. In the core of the application there should be no need to serialize/deserialize data. There we should only have to deal with objects. That will be great, because objects can provide guarantees regarding the data they keep, so they are safer to use than strings. They also have an explicitly defined API, so they are much easier to use.
Of course many developers know this. They’ll use Value Objects to wrap strings, enforcing data consistency and ease of use. And not just strings, because we have several other primitive types at our disposal that support different use cases like doing math.
The problem is, how do we safely go from a string to an integer. To complicate things, most string data gets to us in the form of an associative array (i.e. map) of key/value pairs, both of which are a string. For instance when we get a record from our database abstraction library, it will be an array. If we want to use that data we can access it by its key, but we have to ensure it’s there. The next step is to ensure it’s of the correct type, and optionally cast it to the correct type:
/** @var array $record */ $title = $record['title'];
$numberOfComments = (int)$record['numberOfComments'];
From the type signature if $record
, it’s not clear that we may expect keys title
and numberOfComments
to exist. Even if they exist we can’t be sure that their values are of the expected type. When working with arrays you always have to check if the key exists before accessing it, or you may get a PHP Notice (and hopefully to the error that it really is, but most frameworks nowadays do this for you). We can use the so-called null coalescing operator (??
) to overcome the problem of undefined keys:
/** @var array $record */ $title = $record['title'] ?? '';
$numberOfComments = (int)($record['numberOfComments'] ?? 0);
This works if the key is undefined, but it will also revert to the default value of they key did exist but the value was null
. We lose an important piece of information, namely that the requested key is undefined. In most cases this is a programming mistake, e.g. we forgot to add the column to the SQL SELECT
statement. When using ??
it’s a lot harder to discover this problem because it “swallows” the problem.
Instead we should explicitly assert that the key exists:
/** @var array $record */ if (!array_key_exists($record, 'title')) { throw new LogicException('Expected array $record to have a key "title"');
}
$title = $record['title']; if (!array_key_exists($record, 'numberOfComments')) { throw new LogicException('Expected array $record to have a key "numberOfComments"');
}
$numberOfComments = (int)$record['numberOfComments'];
Of course, this quickly becomes annoying. So we introduce a helper function for this, e.g.
/** @var array $record */ self::assertKeyExists($record, 'title');
$title = $record['title']; self::assertKeyExists($record, 'numberOfComments');
$numberOfComments = (int)($record['numberOfComments'] ?? 0);
The helper function throws that same exception if the key is undefined.
But couldn’t we just define the expected shape of $record
, thereby fixing the issue? E.g.
/** @var array{title: string, numberOfComments: string} $record */
Not really, because it isn’t this method that defines the structure of $record
. It’s the result of fetching a result set from the database, and that doesn’t give us any guarantees about the shape of the array, or the types of the values. There’s a slightly better type we can use though:
/** @var array<string,string|nu
Truncated by Planet PHP, read more at the original (another 4337 bytes)