Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

PHP 7 return type hinting

Tags:

oop

php

php-7

There are already a lot of questions according to PHP 7 return type hinting and why it is not possible to return null if a class was defined as a return type. Like this one: Correct way to handle PHP 7 return types. The answers usually say that it is not a problem as if a function should return a class then returning null is probably an exception anyway. Maybe I have missed somthing but I don't really understand why. For example let's see a simple user class:

class User
{
    private $username;  // mandatory
    private $password;  // mandatory
    private $realName;  // optional
    private $address;   // optional

    public function getUsername() : string {
        return $this->username;
    }

    public function setUsername(string $username) {
        $this->username = $username;
    }

    public function getPassword() : string {
        return $this->password;
    }

    public function setPassword(string $password) {
        $this->password = $password;
    }

    public function getRealName() : string {
        return $this->realName;
    }

    public function setRealName(string $realName = null) {
        $this->realName = $realName;
    }

    public function getAddress() : Address {
        return $this->address;
    }

    public function setAddress(Address $address = null) {
        $this->address = $address;
    }

}

In our application it is totally legal to have a user without a realName and/or without an address. I even can set the realName and address fields to null - even if the solution above is not optimal. And in our profile page I want to display a hint if the address is null (empty).

But this is only 1 example. Our application has more than 100 database tables and corresponding PHP classes and almost all of them have optional fields. Always writing a try catch block instead of simply checking null to handle optional fields seems to be a bit suboptimal for me.

So what is a good solution for the example above?

like image 586
Vmxes Avatar asked Oct 19 '22 11:10

Vmxes


2 Answers

The problem is that your object doesn't follow the OOP rules.

First, getters and setters are evil because they expose the internal structure of the object. You just introduce the infinite possibility to depend on the internal user object structure for the rest of your system. While the purpose of the object is to hide the implementation details and provide the interface to manipulate this hidden data.

And null is evil (or a billion dollar mistake) because it makes the code much less reliable. It introduces the special case (null return value) and it is necessary to handle this special case in the code with uses your object. The worse thing is that if you forget to handle this "null" special case (or if you don't know that there is a special case), you will not get the error immediately. So you will get the hidden error which can be very time-consuming to find and fix.

Practically, I see following options (in all cases - remove the getters):

Option 1) Get the snapshot of the profile data in some unified form

$user->getProfileData() {
    return [
        'username': $this->username,
        'realName': $this->realName ? $this->realName : '',
        'address': $this->address ? $this->address : 'Not specified'
        ...
    ];
}

This is still a kind of a getter, but you transform all the data into the unified form (strings). Even if you have some integer of float fields (like age or height) in your database, you would still return strings here, so the rest of your system doesn't depend on actual internals of the object.

The empty string (or special value like "not specified") for optional fields also acts as a kind of "null object". It's also possible to wrap the returned values into small fields-objects and actually use the null object pattern for optional fields.

In the code which uses the class you should not deal with the special "null" case, you just loop over fields and show strings (including those which are empty).

Option 2) Make the object itself responsible for the presentation

$user->showProfile($profileView) {
    $profileView->addLabel('First Name');
    $profileView->addString($this->username);
    ...
}

Here you keep the internal details inside the object, but now it does too much, so someone could say it now violates the SRP.

Option 3) Create special object responsible for the presentation

$userPresentation = $user->createPresentation()
// internally it will return new UserPresentation($this->username, this->realName, $this->address, ...);
// now display it - generate the template and insert it into the view
<? echo $userPresentation->getHtml(); ?>

Here you move the presentation logic into the separate object. The user object and it's presentation are tightly coupled, but the rest of the system now knows nothing (and has no chance to know) about the user object internals.

like image 65
Boris Serebrov Avatar answered Oct 29 '22 14:10

Boris Serebrov


I have recently embraced the idea that null means there's a problem, so that I know that when I see it, it means something went wrong.

In these situations I tend to use the Null Object design pattern.
So, to handle the address, you would create something like:

class NullAddress implements AddressInterface { }

The Address class would also need to implement AddressInterface.
Then, getAddress() would look like:

public function getAddress(): AddressInterface {
    return $this->address ?? new NullAddress();
}

A function calling getAddress() can then check for a null value by:

if ($user->getAddress() instanceof NullAddress) {
    // Implement result of empty address here.
}

For handling strings, an returning empty string has served me well so far. I just use empty() to check it.

like image 34
Batandwa Avatar answered Oct 29 '22 14:10

Batandwa