📜 ⬆️ ⬇️

New PHP, Part 1: Return types

Each major PHP release adds a number of new features, some of which really matter. For PHP 5.3, these were namespaces and anonymous functions. For PHP 5.4 - traits. For PHP 5.5 - generators. For 5.6, variable-length argument lists.

PHP 7 has a large number of innovations and improvements that make the life of a developer easier. But I believe that the most important and long-term change is working with types. The combination of new features will change the view on PHP development for the better.

Why is strong typing support so important? It provides the compiler or runtime program and other developers with valuable information about what you were trying to do without having to execute the code. This gives three types of advantages:
')
  1. It becomes much easier to communicate the purpose of the code to other developers. It's almost like documentation, only better!
  2. Strong typing gives the code a narrow focus of behavior, which contributes to increased isolation.
  3. The program reads and understands the strict typing just like a person, it becomes possible to analyze the code and find errors for you ... before you execute it!

In the overwhelming majority of cases, explicitly typed code will be easier to understand, it is better structured and has fewer errors than weakly typed code. Only in a small percentage of cases does strong typing cause more problems than good, but do not worry, in PHP, type checking is still optional, as well as before. But in fact, it should always be used.

Return types


The first addition to the existing typing system will be support for returned types. Now you can specify in functions and methods the type of the return value explicitly. Consider the following example:

class Address {  protected $street;  protected $city;  protected $state;  protected $zip;  public function __construct($street, $city, $state, $zip) {    $this->street = $street;    $this->city = $city;    $this->state = $state;    $this->zip = $zip;  }  public function getStreet() { return $this->street; }  public function getCity() { return $this->city; }  public function getState() { return $this->state; }  public function getZip() { return $this->zip; } } 

 class Employee { protected $address; public function __construct(Address $address) {   $this->address = $address; } public function getAddress() : Address {   return $this->address; } } 

 $a = new Address('123 Main St.', 'Chicago', 'IL', '60614'); $e = new Employee($a); print $e->getAddress()->getStreet() . PHP_EOL; // Prints 123 Main St. 

In this rather mundane example, we have an Employee object, which has only one property that contains the email address we passed. Notice the getAddress() method. After the parameters of the function, we have a colon and a type. It is the only type that can take a return value.

Postfix syntax for returned types may seem strange for developers accustomed to C / C ++ or Java. However, in practice, the approach with a prefix declaration is not suitable for PHP, since There may be multiple keywords in front of the function name. In order to avoid problems with the PHP parser, I chose a path similar to Go, Rust and Scala.

When returning any other type using the getAddress() method, PHP will throw a TypeError exception. Even null will not satisfy type requirements. This allows us to refer to print to the method of the Address object with absolute confidence. We will know for sure that an object of this type is indeed returned, not null , not false, not a string, or some other object. This is what ensures the safety of work and the absence of the need for additional checks, which in turn makes our own code cleaner. Even if something goes wrong, PHP will definitely warn us.

But what if we have a less trivial case and need to handle situations when there is no Address object? We introduce the EmployeeRepository , the logic of which allows you to have no records. First we add the ID field to the Employee class:

 class Employee { protected $id; protected $address; public function __construct($id, Address $address) { $this->id = $id; $this->address = $address; } public function getAddress() : Address { return $this->address; } } 

Now create our repository. (As a stub, we will add dummy data directly to the constructor, but in practice, of course, you need to have a data source for our repository).

 class EmployeeRepository { private $data = []; public function __construct() { $this->data[123] = new Employee(123, new Address('123 Main St.', 'Chicago', 'IL', '60614')); $this->data[456] = new Employee(456, new Address('45 Hull St', 'Boston', 'MA', '02113')); } public function findById($id) : Employee { return $this->data[$id]; } } 

 $r = new EmployeeRepository(); print $r->findById(123)->getAddress()->getStreet() . PHP_EOL; 

Most readers will quickly notice that `findById ()` has a bug, because if we ask a non-existent employee ID, PHP will return `null` and our` getAddress () `call will die with the error" method called on non-object ". But actually the mistake is not there. The reason is that `findById ()` must return the employee. We specify the return type `Employee` so that it is clear whose error it is.

What to do if there really is no such employee? There are two options: the first is an exception; if we cannot return what we promise, this is a reason for special handling outside the normal flow of the code. The other is an indication of the interface whose implementation will be returned (including the “empty” one). Thus, the rest of the code will work and we will be able to control what is happening in the "empty" cases.

The choice of approach depends on the use case and is also determined by the scope of the inadmissibility of consequences if the correct type is not returned. In the case of a repository, I would argue with the choice in favor of exceptions, since the chances of getting into such a situation are minimal, and working through exceptions is rather expensive in terms of performance. If we were dealing with a scalar variable, then processing the "empty value" would be an acceptable choice. We modify our code accordingly (for brevity, only the changed parts are shown):

 interface AddressInterface { public function getStreet(); public function getCity(); public function getState(); public function getZip(); } 

 class EmptyAddress implements AddressInterface { public function getStreet() { return ''; } public function getCity() { return ''; } public function getState() { return ''; } public function getZip() { return ''; } } 

 class Address implements AddressInterface { // ... } 

 class Employee { // ... public function getAddress() : AddressInterface { return $this->address; } } 

 class EmployeeRepository { // ... public function findById($id) : Employee { if (!isset($this->data[$id])) { throw new InvalidArgumentException('No such Employee: ' . $id); } return $this->data[$id]; } } 

 try { print $r->findById(123)->getAddress()->getStreet() . PHP_EOL; print $r->findById(789)->getAddress()->getStreet() . PHP_EOL; } catch (InvalidArgumentException $e) { print $e->getMessage() . PHP_EOL; } /* * Prints: * 123 Main St. * No such Employee: 789 */ 

Now getStreet() will return a nice empty value.

One important note about return types: during inheritance, the type cannot be changed, it cannot even be made more specific (a subclass, for example). The reason is in the features of lazy PHP download.

Return types are a big, but far from the only, new feature that extends the PHP type system. In the second part, we consider another, perhaps even more important, change: declaring scalar types.

Source: https://habr.com/ru/post/267257/


All Articles