PHP 7[.2]

The New New PHP

Presented by Larry Garfield (@Crell)

@Crell

Larry implements Huggable
PHP 7

Show me the data!

Most developers are now on PHP 7

(Credit: https://seld.be/notes/php-versions-stats-2018-1-edition)

You're missing

But most packages target PHP 5.

(Credit: https://seld.be/notes/php-versions-stats-2018-1-edition)

#1

Do you trust your docblock?


class User {
  protected $id;
  protected $address;

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

function compute_shipping($src, User $user) {
  $dest = $user->getAddress();

  $distance = distance_between($src, $dest);

  return $distance * .5; // 50 cents/mile
}
        

class User {
  protected $id;
  protected $address;

  /**
   * @return Address
   */
  public function getAddress() {
    return $this->address;
  }
}

function compute_shipping($src, User $user) {
  $dest = $user->getAddress();

  $distance = distance_between($src, $dest);

  return $distance * .5; // 50 cents/mile
}
        

class User {
  protected $id;
  protected $address;

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

function compute_shipping($src, User $user) {
  $dest = $user->getAddress();

  $distance = distance_between($src, $dest);

  return $distance * .5; // 50 cents/mile
}
        

Return types

  • New in PHP 7!
  • Not nullable until 7.1

$r = new UserRepository();

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

          Method called on non-object
        

No NULL means:

  • Exception
  • "Empty" value object

For Repositories etc.


class UserRepository {
  // ...

  public function findById($id) : User {
    if (/* can't find the user */) {
     throw new InvalidArgumentException('No such User: ' . $id);
    }
    return $user;
  }

}
        

For Value objects


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 {
    // ...
}
        

Returning false on error is a
big 'screw you' to your users

But in 7.1...


class UserRepository {
  // ...

  public function findById($id) : ?User {
    if (/* can't find the user */) {
     return null;
    }
    return $user;
  }

}
        

(Please don't)

void returns


function return_to_sender() : void {
  // Do stuff
  // ...
  return;      // Legal
  return null; // Not OK
  return 1;    // Not even a little OK
}
        

#2


function compute_shipping($src, AddressInterface $dest) {
  $distance = distance_between($src, $dest);

  return $distance * .5; // 50 cents/mile
}

function distance_between(AddressInterface $src, AddressInterface $dest) {
  // ...
}

$u_address = new Address(); // ...
$user = new User(123, $u_address);

$src = new Address(); // ...
compute_shipping($src, $user->getAddress());
        

What does distance_between() return?


function compute_shipping(AddressInterface $src, AddressInterface $dest) : float {
  $distance = distance_between($src, $dest);

  return $distance * .5; // 50 cents/mile
}

function distance_between(AddressInterface $src, AddressInterface $dest) : int {
  // ...
}

$u_address = new Address(); // ...
$user = new User(123, $u_address);

$src = new Address(); // ...
compute_shipping($src, $user->getAddress());
        

Everyone here owes
Andrea Faulds and Anthony Ferrara a drink

New type specs in PHP 7

  • int
  • float
  • string
  • bool

Full type list

  • int
  • float
  • string
  • bool
  • array
  • callable
  • object
  • Any class name

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

Is getStreet() a string or object?


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

class Address implements AddressInterface {
  public function __construct(string $street, string $city, string $state, string $zip) {
    // ...
  }
  // ...
}
        

$u_address = new Address('45 Hull St', 'Boston', 'MA', 02215);
$user = new User(123, $u_address);

$src = new Address('1060 W Addison St', 'Chicago', 'IL', 60613);
compute_shipping($src, $user->getAddress());
        

Strict vs Weak mode

Weak by default

* string -> int may E_NOTICE

Strict mode

Address.php


declare(strict_types=1);

class Address implements AddressInterface {
  public function __construct(string $street, string $city, string $state, string $zip) {
    // ...
  }
  public function getZip() : string {
    return $this->zip;
  }

  // ...
}
        

Affects calls and returns from this file

Strict mode

setup.php


declare(strict_types=1);

$u_address = new Address('4 Yawkey Way', 'Boston', 'MA', 02215);
$user = new User(123, $u_address);

$src = new Address('1060 W Addison St', 'Chicago', 'IL', 60613);
compute_shipping($src, $user->getAddress());
        

Affects calls and returns from this file

Casting in strict mode

* int converts to float automatically

Using strict types

90% of the time

(Everywhere except Input)

Also, Documentation

BC Warning!

New reserved words!

#3


usort($array, function ($a, $b) {
  if ($a < $b) {
    return -1;
  }
  elseif ($a > $b) {
    return 1;
  }
  else {
    return 0;
  }
});
        

T_SPACESHIP

<=>


usort ($array, function($a, $b) {
  return $a <=> $b;
});
        

usort($people, function (Person $a, Person $b) {
  if ($a->lastName() < $b->lastName()) {
    return -1;
  }
  elseif ($a->lastName() > $b->lastName()) {
    return 1;
  }
  else {
    if ($a->firstName() < $b->firstName()) {
      return -1;
    }
    elseif ($a->firstName() > $b->firstName()) {
      return 1;
    }
    else {
      return 0;
    }
  }
});
        

usort($people, function (Person $a, Person $b) {
  return $a->lastName() < $b->lastName() ? -1
    : ($a->lastName() > $b->lastName() ? 1
      : $a->firstName() < $b->firstName() ? -1
        : ($a->firstName() > $b->firstName() ? 1 : 0));
});
        

You're Fired!


usort($people, function (Person $a, Person $b) {
 return [$a->lastName(), $a->firstName()] <=> [$b->lastName(), $b->firstName()];
});
        

#4


$username = $username ? $username : 'Anonymous';
        

$username = $username ?: 'Anonymous';
        

But what if $username doesn't exist?


Notice: Undefined variable: username on line 3
        

$username = isset($username) && !is_null($username) ? $username : 'Anonymous';
        

NULL coalesce!


$username = $username ?? 'Anonymous';
        

$username = $submitted['username'] ?? $user->username() ?? 'Anonymous';
        

?? checks setness, not truthiness

#5

Cryptography is hard

Don't we have this already?

  • rand() isn't really random
  • mcrypt() isn't really random (also unsupported)
  • fread(fopen('/dev/urandom'), 16) isn't portable
  • openssl is hard to use
Picard-Riker Face Palm

What we want is an easy
Cryptographically Secure Pseudo-Random Number Generator

CSPRNG!

Ask and ye shall receive…


$junk = random_bytes(16);

$val = random_int(1, 100);
       

On PHP 5?

composer require paragonie/random_compat

https://github.com/paragonie/random_compat

If you're not using these functions for security...

  • password_hash() / password_verify()
  • random_bytes()
  • random_int()

You're Doing It Wrong!

BC Warning!

Reserved function names

  • random_bytes()
  • random_int()
BC Warning

#6

Mocking is hard


$mock_repository = $this
  ->getMockBuilder(ThingieRepository::class)
  ->disableOriginalConstructor()
  ->getMock();

$obj1 = new Thingie();
$obj1->setVal('abc');

$obj2 = $this->getMockBuilder(Thingie::class)
  ->disableOriginalConstructor()
  ->getMock();
$obj1 ->method('getVal')->willReturn('def');

$map = [ [1, $obj1], [2, $obj2] ];

$mock_repository
  ->method('load')->will($this->returnValueMap($map));

$subject = new ClassUnderTest($mock_repository);
$this->assertTrue($subject->findThings());
         

Fakes are easy


$fake_repository = new class extends ThingieRepository {
  public function __construct() {}

  public function load($id) {
    switch ($id) {
      case 1:
        $obj1 = new Thingie();
        $obj1->setVal('abc');
        return $obj1;
      case 2:
        return new class extends Thingie {
          public function getVal() {
            return 'def';
          }
        };
    }
  }
};

$subject = new ClassUnderTest($fake_repository);
$this->assertTrue($subject->findThings());
         

One-off dependencies


new Service(new class implements LoggerInterface {
  use LoggerTrait;

  public function log($message) {
    print $message . PHP_EOL;
  }
});
         

Constructors, too


new class($logger) implements ServiceInterface {
  public function __construct(LoggerInterface $logger) {
    $this->logger = $logger;
  }

  public function doServiceStuff() { }
};
         

#7

Fatal errors suck

Recoverable errors… aren't

PHP 5


function doStuff(Request $r) {
  // ...
}

$u = new User();

doStuff($u);
        

Catchable fatal error: Argument 1 passed to doStuff() must be an instance of Request,
instance of User given

PHP 7


function doStuff(Request $r) {
  // ...
}

$u = new User();

try {
  doStuff($u);
}
catch (\TypeError $e) {
  print "Wrong variable type, dummy." . PHP_EOL;
}
        

try {
  include 'buggy_file.php';
}
catch (\ParseError $e) {
  $logger->error("That file is buggy!");
}
        

try {
  nonexistant_function();
}
catch (\Error $e) {
  print "The error was " . $e->getMessage();
}
        
Throwable is at the root of all Errors and Exceptions

E_FATAL, E_RECOVERABLE_ERROR, E_PARSE, E_COMPILE_ERROR

Exceptions: The user screwed up

Errors: The coder screwed up

BC Warning!

BC Warning

New reserved class names

Global exception handler

PHP 5


set_exception_handler(function(\Exception $e) {
  // ...
});
        

PHP 7


set_exception_handler(function(\Throwable $e) {
  // ...
});
          

PHP 5 & 7


set_exception_handler(function($e) {
  // ...
});
          

#8

Assertions suck


function doStuff(array $def) {
  assert(isset($def['key']) && isset($def['value']), 'Invalid def');
  // ...
}
        

function doStuff(array $def) {
  assert('isset($def[\'key\']) && isset($def[\'value\'])', 'Invalid def');
  // ...
}
          

PHP 7: Expectations


ini_set('assert.exception', 1);
ini_set('zend.assertions', 1);

class InvalidDef extends AssertionError {}

function doStuff(array $def) {
  assert(isset($def['key']) && isset($def['value']), new InvalidDef('Invalid def'));
  // ...
}
        

Assertions are actually not-broken!

#9

Iterate this!

PSR-8 FTW!


function hug_friends(array $friends) {
  foreach ($friends as $friend) {
    hug($friend);
  }
}
        

function hug_friends(\Traversable $friends) {
  foreach ($friends as $friend) {
    hug($friend);
  }
}
        

There can be only one!

Your friends are sad :-(


/**
 * @param array|\Traversable $friends
 */
function hug_friends($friends) {
  foreach ($friends as $friend) {
    hug($friend);
  }
}
        

PHP 7.1 makes friends happy


function hug_friends(iterable $friends) : iterable {
  foreach ($friends as $friend) {
    hug($friend);
  }
  return $friends;
}
        

is_iterable([1, 2, 3]); // true
is_iterable(new ArrayIterator([1, 2, 3])); // true
is_iterable((function () { yield 1; })()); // true
is_iterable(1); // false
is_iterable(new stdClass()); // false
        

iterable => foreach()-able

#10

What does this do?


$$foo['bar']['baz']
        

$foo->$bar['baz']
        

$foo->$bar['baz']()
        

Foo::$bar['baz']()
        

Foo::$bar[1][2][3]()
        

Uniform variable syntax

Always Left-To-Right

It's logical


$foo()['bar']()
[$obj1, $obj2][0]->prop
getStr(){0}
$foo['bar']::$baz
$foo->bar()::baz()
$foo()()
        

(...)['foo']
(...)->foo
(...)->foo()
(...)()

(function() { ... })()
($obj->closure)()
        

BC Break

BC Break

Complex cases change meaning

When in doubt, use () or {}

11

(Credit: https://www.flickr.com/photos/martin_heigan/4544138976/)

Credit: Rasmus Lerdorf

Credit: Rasmus Lerdorf

Credit: Rasmus Lerdorf

Credit: Rasmus Lerdorf

Credit: Rasmus Lerdorf

Credit: Rasmus Lerdorf

Credit: Rasmus Lerdorf

But I have legacy code!

You have tests, right...?

Tooling to the rescue

  1. Get https://github.com/squizlabs/PHP_CodeSniffer
  2. Get https://github.com/wimg/PHPCompatibility
  3. vendor/bin/phpcs --standard=PHPCompatibility .
  4. Buy Wim Goden (@wimgtr) a $beverage
  5. Profit!1!eleven!

oldAndBusted.php


function random_int()
{
    return rand();
}

class Error {}

$a = [1, 2, 3];
$b = ['a', 'b', 'c'];

print $$b[1];
          

composer.json


{
    "require-dev": {
        "squizlabs/php_codesniffer": "^2.2 || ^3.0.2",
        "wimg/php-compatibility": "*",
        "DealerDirect/phpcodesniffer-composer-installer": "^0.4.3"
    },
    "prefer-stable" : true
}
          

Profit


$ vendor/bin/phpcs --standard=PHPCompatibility oldAndBusted.php

FILE: /home/crell/temp/phptest/oldAndBusted.php
---------------------------------------------------------------------------------------
FOUND 1 ERROR AFFECTING 1 LINE
---------------------------------------------------------------------------------------
 11 | ERROR | Indirect access to variables, properties and methods will be evaluated
    |       | strictly in left-to-right order since PHP 7.0. Use curly braces to
    |       | remove ambiguity.
---------------------------------------------------------------------------------------

Time: 60ms; Memory: 6Mb
          

Caveats

  • Large directories can be slow
  • Can only test up to the installed version
  • Static analysis can't catch everything

Tools can do 80% of the work

The market is already there

http://phpversions.info/

PHP 7.2-ready hosts: 22 and counting

(Yes, Platform.sh is one of them!)

Require PHP 7.1.x

  • Symfony 4
  • Larvel
  • Doctrine
  • phpMyAdmin
  • Zend Framework new packages

Require PHP 7.0.x

  • Magento

Time is running out...

PHP 7.0 and earlier lose all support this year.
PHP 7

Larry Garfield

Director of Developer Experience, Platform.sh

Continuous Deployment Cloud Hosting

Stalk us at @PlatformSH

Thanks to Palantir.net for helping develop these slides