WPLake > Learning Hub > PHP Typed: Tiny Composer Package That Breaks PHP Rules For You

PHP Typed: Tiny Composer Package That Breaks PHP Rules For You

Sounds too loud? Let’s clarify to avoid disappointing expectations: This package uses some magic outside of Hogwarts and will be quite useful for fans of strict types in PHP.

Table of Contents

1. Introduction

Hey everyone! We're WPLake, a WordPress development agency.

With the New Year just around the corner, it seems like everyone is taking a moment to reflect on the year that’s coming to a close. What better time to share some solutions to common challenges we've faced this year and found to be incredibly useful?

This post is about PHP Typed — a Composer package (wplake/typed) we published this week. Let’s dive in!

2. Challenges of Loose Type Handling in PHP

PHP, being an interpreted language without strict typing, was designed to simplify life for developers and make development easier. With no compilation and built-in garbage collection, it promised to be a paradise for developers.

But, as with everything, there are two sides to the coin. It didn’t take long for everyone to realize that while PHP’s loose typing made development faster, it also made code less safe and harder to maintain.

For example:

function getUserData($id) {}

What’s going on here? Does the function expect an int as $id, or maybe a string token? What does it return? An object? An array? You can’t know for sure without looking at the implementation.

That’s why PHPDoc was created:

/**
 * @var int $id
 * @return array
 */
function getUserData($id) {}

Okay, this is much better. PHPDoc became the de facto industry standard for documenting code in commercial software. Moreover, with PHP 7.4 and beyond, the language has made strides toward better type support.

However, we still encounter many typing-related issues. Let’s look at a couple of examples of common, verbose functions written while dealing with untyped variables:

function getUserAge(array $userData): int
{
    return isset($userData['meta']['age']) &&
           is_numeric($userData['meta']['age'])
           ? (int) $userData['meta']['age']
           : 0;
}

function upgradeUserById($mixedUserId): void
{
    $userId = is_string($mixedUserId) || 
         is_numeric($mixedUserId)
        ? (string)$mixedUserId
        : '';
}

While these functions are not useful in themselves, they demonstrate the verbosity and lack of clarity that often arise when dealing with untyped variables.

"Hold on. Why not just declare a type for $mixedUserId? Why not add PHPDoc comments to describe the keys of $userData?"
You might be thinking that as you read these examples.

In an ideal world, yes, we would declare a string type for $mixedUserId, and $userData would be either a class instance or at least an array with the keys described in PHPDoc. The reality, however, is that software isn’t written by a single developer and doesn’t consist of just 100 lines of code. There are third-party components, external libraries, vendors, and large legacy codebases to consider.

When it comes to arrays, we have a strong feeling that this issue will persist for years to come. It’s nearly impossible to guarantee a fixed structure for arrays — just think of $_SERVER, where values can differ depending on the environment, not to mention user-driven inputs like $_GET and $_POST.

Okay, enough about the problems — let’s talk about solutions.

3. Default Approaches for Type Casting

How can we tackle the typecasting issue? There are three common approaches out of the box:

3.1) Using "isset" and type check

As we saw above:

$age = isset($userData['meta']['age']) &&
             is_numeric($userData['meta']['age'])
           ? (int) $userData['meta']['age']
           : 0;

This is verbose and unclear even for such a simple task.

3.2) Using the null-coalescing operator and type check

$number = $data['meta']['age'] ?? 10;
$number = is_numeric($number) ? (int) $number : 10;

What’s happening here? We end up with two lines of code because the null-coalescing operator doesn't address type-checking. Additionally, when using a custom default value with the null coalescing operator, you end up repeating yourself.

3.3) Using the null-coalescing operator and straight type casting

$number = (int) ($data['meta']['age'] ?? 10);

That’s it! Short and sweet! Some of you might be thinking, "This post is just wasting my time."

Then our answer: Wait a minute. Let's take a deeper look at what’s happening here. This line consists of two parts:

Part 1: $data['meta']['age'] ?? 10

We safely check if the variable is present and provide a default value if not. Cool, let's move on.

Part 2: (int) {resultFromNullCoalescing}

Here, we tell PHP to cast the array item (or our default value) to an integer. This seems straightforward, but let’s think about the type of variable we’re casting to an int.

Whoops, but we actually don’t know the type.

What? Yes, while writing this code, you probably "expected" that the '[meta][age]' item would either be a string or at least a numeric string. But can you guarantee it? In the real world, the answer is no. Large applications have countless logic branches, and one of them could affect this value.

Moreover, even if it's an integer now, who knows what it will be tomorrow? The app gets an update, and now the 'age' field is an object with several properties. Boom! This line produces a fatal error because PHP can’t convert an object to a string unless it explicitly implements __toString.

Another case: array to string conversion

$string = (string) ($array['meta']['location'] ?? 'Default one');

When the 'location' turned to be an array, instead of the expected string, the script will:

  1. Produce a notice
  2. Continue execution with the string, containing literally 'Array'

While obviously wrong data is being used, in this case, the proper logic would use the fallback — 'Default one'.

“That’s not my fault. Let it be a fatal error, let it be wrong logic.”
(Some careless developer might say)

But should it actually be a fatal error? If this is just a small branch showing insignificant labels on the screen, should it really cause the entire app to fail in production?

If you've worked with external data vendors, you know how unpredictable their updates can be.

A good programmer isn’t careless. At least, not in the eyes of their boss, and in the better world of development practices. Let’s not be the bad guys.

While the first two approaches are safe, they’re cumbersome and verbose. Now, let's finally explore what the Typed package has to offer in assisting with this task.

4. PHP Typed: Utility for Seamless Type-Casting

So, how can we improve this? Based on our research, there isn’t a well-known package that solves this exact task. However, there are several array wrappers that simplify array-related tasks, such as Laravel Collections.

Wrapping items into arrays sounds good and works well within the framework ecosystem, but outside of it, we can’t expect everyone to use that package. Using an entire wrapper to extract a single variable is obviously excessive.

So we need a pure solution. The good news is that the research led to one important discovery: an elegant approach for handling inner keys.

Since we’re primarily working with arrays and improving type casting, it would be great to replace constructions like $data['meta']['location']['city'] with something cleaner, such as meta.location.city, similar to how Laravel’s Arr::get works.

Now, let's formulate the logic for the helper function:

"Give me the value of the requested type from the given source by the specified path, or return the default value."

Sorted. Now, let’s talk about how it should look. It should be simple, intuitive, and clear — even for those unfamiliar with our utility.

The original construction, while unsafe, is quite expressive:

$number = (int)($data['meta']['age'] ?? 10);

Can we come up with something similar? After much thought, many checks, and creating the package —yes! Here’s the result:

$number = int($data, 'meta.age', 10);

Looks almost the same, right? But wait—what’s this "int" wrapped around it? Isn't it a reserved type keyword in PHP? Here’s the discovery, the "breaking of the rules" mentioned in the post title:

PHP allows functions to use the same names as variable types.

Were you sure it was prohibited? Not quite! While certain names are restricted for classes, interfaces, and traits, function names are not:

“These names cannot be used to name a class, interface, or trait” - PHP Manual: Reserved Other Reserved Words

This means we can have things like string($array, 'key'), which resembles (string)$array['key'] but is safer and smarter—it even handles nested keys and default values.

The package supports both PHP 7.4+ and 8.0+, and is distributed via Composer, so the installation process goes as usual:

composer require wplake/typed

// then in your app:
require __DIR__ . '/vendor/autoload.php';

By the way, importing these functions does not interfere with native type casting in PHP. So, while practically useless, the following will still work:

echo (string)string('hello');

The Typed package provides a set of helper functions under the WPLake/Typed namespace, so you don’t need to worry about potential global conflicts. The code, with imports, looks like this:

use function WPLake\Typed\string;

$string = string($array, 'first.second', 'default value');

Of course, your IDE will automatically handle the use line for you. For those who can't stand functions, the package also offers static methods:

use WPLake\Typed\Typed;

Typed::int($data, 'key');

As usual, there’s a fly in the ointment: unlike other types, the array keyword falls under a different category, which prohibits its use for function names. That’s why, in this particular case, we used the name arr instead.

5. PHP Typed: Usage Examples

Let's take a single function from the Typed package and review usage examples on it. Let it be the mentioned string. Here’s the function declaration for string():

namespace WPLake\Typed;

/**
 * @param mixed $source
 * @param int|string|array<int,int|string>|null $keys
 */
function string($source, $keys = null, string $default = ''): string;

Usage Scenarios

5.1) Extract a string from a mixed variable

By default, returning an empty string if the variable can't be converted to a string:

$userName = string($unknownVar);
// you can customize the fallback:
$userName = string($unknownVar, null, 'custom fallback value');

5.2) Retrieve a string from an array

Including nested structures (with dot notation or as an array):

$userName = string($array, 'user.name');
// Alternatively:
$userName = string($array, ['user', 'name']);
// custom fallback:
$userName = string($array, 'user.name', 'Guest');

5.3) Access a string from an object

Including nested properties:

$userName = string($companyObject, 'user.name');
// Alternatively:
$userName = string($companyObject, ['user', 'name']);
// custom fallback:
$userName = string($companyObject, 'user.name', 'Guest');

5.4) Work with mixed structures

(e.g., object->arrayProperty['key']->anotherProperty or ['key' => $object]

$userName = string($companyObject, 'users.john.name');
// Alternatively:
$userName = string($companyObject, ['users', 'john', 'name']);
// custom fallback:
$userName = string($companyObject, 'users.john.name', 'Guest');

In all the cases, the fallback value is the 'empty' value for the specific type (e.g. 0, false, "", and so on), but you can pass a custom default value as the third argument:

$userName = string($companyObject, 'users.john.name', 'Guest');

The package includes functions for the following types:

  • string
  • int
  • float
  • bool
  • object
  • dateTime
  • arr (stands for array, because it's a keyword)
  • any (allows to use short dot-keys usage for unknowns)

Additionally:

  • boolExtended (true,1,"1", "on" are treated as true, false,0,"0", "off" as false)
  • stringExtended (supports objects with __toString)

For optional cases, when you need to apply the logic only when the item is present, each function has an OrNull variation (e.g. stringOrNull, intOrNull, and so on), which returns null if the key doesn’t exist.

6. Conclusion

We’ve already applied the PHP Typed package in one of our real-world applications, and we’ve found that it significantly improved the clarity, elegance, and intuitiveness of our code. We hope it will be just as useful for others in the PHP community.

Note: If you found this package helpful, please consider giving its GitHub repository a star and sharing it with your colleagues to help them improve their code as well.

Thank you for taking the time to read through this post! We wish you a wonderful New Year filled with success and growth in all your coding endeavors.

P.S. We’ve some more news: this week, we’ve released another package that could be valuable for PHP developers. We’ll publish a post about it next week, but if you’re eager to check it out now, the package is already available, and here’s the link to explore it!

Stuck with development or facing an issue?

WPLake offers affordable on-demand website development and design.

No matter the size of your project - contact us now, and we'll get it done for you!

Get assistance now
Was this article helpful?

Totally useless

Slightly helpful

Very helpful

Related articles

Content links (12)

About the Author

Maxim Akimov

Certified WordPress expert from Ukraine with over 8 years of experience. Advocate of the BEM methodology and the overall modular approach. Loves sporting activities and enjoys going to the gym and regularly plays table tennis.

0 Comments

    Leave a comment

    Reply to 

    Please be considerate when leaving a comment.

    Not shown publicly


    Got it