PHP 7 is (almost) here – OMG! PANIC! – Adam Harvey @LGnome New Relic



PHP 7 is (almost) here – OMG! PANIC! – Adam Harvey @LGnome New Relic

0 0


php7-drupalcon

PHP7 is (almost) here! OMG! Panic! (DrupalCon Barcelona version)

On Github LawnGnome / php7-drupalcon

PHP 7 is (almost) here

OMG! PANIC!

Adam Harvey @LGnome New Relic

What is PHP 7?
Well, firstly with apologies to Vincent Pontier, there's meant to be a space! (Although that doesn't hashtag as well.)
5.0.02004-07-135.1.02005-11-245.2.02006-11-025.3.02009-06-305.4.02012-03-015.5.02013-06-205.6.02014-08-28 Let's start with where we came from. PHP 5 was first released in July 2004 (11 years ago!), and then really snapped into focus as a modern language in 2009 with PHP 5.3.
5.0.02004-07-135.1.02005-11-245.2.02006-11-025.3.02009-06-305.4.02012-03-015.5.02013-06-205.6.02014-08-28 In 2012 we started releasing new versions annually. (Ish.) Each of these included new features, but also shone a light on how many things we'd like to add, remove, deprecate and clean up that would break BC.
I stirred the pot a bit early last year…
…but then in May this "phpng" branch with several hundred commits appeared out of nowhere and confused Sebastian.
It was the culmination of a four month sprint (oxymoron?) by Dmitry Stogov, Xinchen Hui and Nikita Popov, sponsored by Zend, to improve performance. This was obviously the base for a new major version.
And so the floodgates opened.

45 RFCs

9,685 commits

180 contributors

We ended up accepting 45 RFCs. Big ones. Small ones. Momentous ones. (page down) Lots and lots of commits. And that contributor count is easily the highest ever, and doesn't even count non-code contributors.

It's faster: phpdoc

So that's the what. Let's talk about the why. Well, number one: as mentioned, it's faster. Building php-doc. Never trust a benchmark you didn't fake yourself.

It's faster: typical blog

I know this is a Drupal conference, but this one was mind-blowing enough that I had to check if I'd messed something up.

It's faster: Drupal 8

Drupal 8 beta 15 shows a less marked improvement, although I suspect that's mostly my test rig: database access over TCP probably isn't helping.

It's smaller: Drupal 8

But here's an extra win: PHP 7 is considerably lighter on memory usage than PHP 5.

It's better

45 RFCs, as noted earlier. I'll talk a little about features, but there's lots I won't be covering today.
If that was the carrot, here's the stick. We have a problem: PHP (mostly 5) is popular. Really popular. 140k GitHub projects. Tens of millions of sites. (Many of which are built on Drupal.)
PHP 5 isn't long for this world.

PHP 5.6 is EOL onAugust 28, 2017

Or, what that really means is…

PHP < 7 is EOL onAugust 28, 2017

that's in 1 year, 11 months, 4 days

That's your changeover period. If you're working on distributed code: modules, themes, Drupal itself: you have to support PHP 7 by the end of that period. Probably much sooner than that. (Drupal 8!)
OH GOD WE'RE ALL DOOMED
OK, so we're not flailing about any more. What should we do instead?
You can just rely on distribution packages to buy a few more years.

Distribution packages

Ubuntu 14.04: PHP 5.5.9, supported to April 2019

RHEL 7: PHP 5.4.16, supported to June 2024

Red Hat's support runs for another nine years. Think about where you were in 2006. (Plus, it won't help for Drupal 8.)
You can have a flag day and just change over. This doesn't work so well if you're an author of things other people use, though.
Or you can dual wield PHP 5 and 7. A cautionary tale:

BC theory

A brief digression into language design. Breaking BC is tricky, because it's likely that your users will want to run code on both the old and new versions. (I mean, I just said you should.) Old &brokenNew &shinyCode that supports both

BC theory

Old &brokenNew &shinyCode that supports both You want to maximise the amount of code in the old version that doesn't have to change to work in the new version — it can be deprecated, but not removed — and minimise how many new features are incompatible with old code.
What happens if you don't do that? Python 3.0 came out almost seven years ago, and there are still 15% of packages unsupported on 3. Adoption has been rocky because of the difficulty in supporting both versions. PHP 7 (hopefully) won't have that problem.
We've strived to keep that intersection as big as possible. And that results in tweets like this. Which are awesome, even if they kind of undermine the entire concept of the presentation.
That's what you can do. What option should you choose? That depends on who you are and what you do.

Module and library authors

TELL YOUR USERS. Support both versions for a reasonable period. Document that. Tell your users more.

Module and library users

(also known as everyone)

Look for support policies. You can either have a flag day or support both, but if you're in a team (n>1) environment, the latter is better, even if it's for a shorter period.

Don't be afraid of making changes to third party code to support PHP 7, but do be afraid of maintaining long lived forks. You need to get them upstream.
So, how do we do this?

Testing

If you don't have regression tests, get writing. These are the single biggest weapon you have to combat BC breaks. If you use PHPUnit, you can use code and branch coverage stats to guide you in terms of whether you're testing enough.

Tooling

php7mar

phan

Tools are starting to appear that can analyse your code to look for problems. Alexia Smith wrote php7mar (Migration Assistant Report); Rasmus wrote phan, which is a more general static analyser that includes detection for some BC breaks.

Try it!

Install PHP 7 in a development/CI environment with E_ALL. See what breaks. Rinse and repeat.
OK, so what has PHP 7 broken? (and how do we find them and fix them so they'll work on PHP 7, or preferably both)

Variable handling

This sounds bad. It's both better and worse than you think.

Indirect references

$$foo['bar']['baz']
$foo->$bar['baz']
$foo->$bar['baz']()
Foo::$bar['baz']()
These have all changed behaviour in PHP 7. Silently. Insidiously. I'll break these down one at a time in a minute, but first…

Why?

…why are we inflicting this pain upon you, the developer?
$foo()['bar']()
$foo::$bar::$baz
Foo::bar()()
(function() { ... })()
($obj->closure)()
[$obj, 'method']()
These — and many other variations — are now supported in PHP 7.0. Check the uniform variable syntax RFC. The "iffy" (à la JavaScript) is particularly awesome.
Obviously, this is awesome. But the pain. On the bright side, this is one of those things that's really rare: when Nikita was working on this, he only found one instance in all of Symfony and Zend Framework where this mattered.
$$foo['bar']['baz'];

in PHP 5:

${$foo['bar']['baz']};
This is interpreted in PHP 5 as "take the value of $foo['bar']['baz'] and use that as a variable name". (Of course, if you're using variable variables anyway, you have already lost.)
$$foo['bar']['baz'];

in PHP 7:

($$foo)['bar']['baz'];
PHP 7: take the value of $foo, use that as a variable name, and then access ['bar']['baz'].
$foo->$bar['baz']

in PHP 5:

$foo->{$bar['baz']}
This is interpreted in PHP 5 as "take the value of $bar['baz'], and use that as a property name".
$foo->$bar['baz']

in PHP 7:

($foo->$bar)['baz']
PHP 7: take the value of $foo->bar, and then access the baz variable. There's also the same thing for method calls (which was the third variation shown earlier).
Foo::$bar['baz']()

in PHP 5:

Foo::{$bar['baz']}()
This is interpreted in PHP 5 as "take the value of $bar['baz'], and use that as a static property name". This is probably the most common variant of the four.
Foo::$bar['baz']()

in PHP 7:

(Foo::$bar)['baz']()
PHP 7: Take the value of the static $bar property of Foo, then get the array value, then call that callable.

What do I do?

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

Using curly braces:

Foo::{$bar['baz']}()
The earlier examples with parentheses and/or curly braces will work for preserving PHP 5 behaviour.
Foo::$bar['baz']()

Break it down:

$method = $bar['baz'];
Foo::$method();
I'd prefer this for clarity.

What do I look for?

Tooling

Once again, tooling is likely to be your friend here. The aforementioned php7mar and phan can detect these. I would hope that IDEs will get support too.
git grep -E '((((::)|(->))\$[a-zA-Z_][a-zA-Z0-9_]*)+\[[^\s]+\]\())|(->\$[a-zA-Z_][a-zA-Z0-9_]*\[)|(\$\$[a-zA-Z_][a-zA-Z0-9_]*\[)'

or with GNU grep:

grep -E '((((::)|(->))\$[a-zA-Z_][a-zA-Z0-9_]*)+\[[^\s]+\]\(\))|(->\$[a-zA-Z_][a-zA-Z0-9_]*\[)|(\$\$[a-zA-Z_][a-zA-Z0-9_]*\[)'
I'll tweet this later. This isn't perfect (doesn't support non-ASCII variable names), but will probably catch most sins. (end of section)

Engine exceptions

In PHP 5, there is a very hard line between errors and exceptions. In PHP 7, where possible, the runtime will throw exceptions for errors — including some that were previously unrecoverable.

PHP 5

E_ERROR: 182E_RECOVERABLE_ERROR: 17E_PARSE: 1

PHP 7

E_ERROR: 54 (-128)E_RECOVERABLE_ERROR: 3 (-14)E_PARSE: 0 (-1)

E_PARSE still exists, but only for the case where you don't catch a ParseError.
function square(int $n): int {
  return $n * $n;
}

square('foo');
Here's an example of code that will generate a TypeError.
Fatal error: Uncaught TypeError:
Argument 1 passed to square() must
be of the type integer, string given
try {
  square('foo');
} catch (TypeError $e) {
  // Recover gracefully.
  // (or show a graceful error)
}
You can catch these error exceptions like any other exception.

PHP 5

Exception
  LogicException
  RuntimeException

PHP 7

Throwable
  Error
    ParseError
    TypeError
    ...
  Exception
    LogicException
    RuntimeException
There are five errors in total, but you can check the manual for all of them. I would expect the list to grow considerably in PHP 7.1 and beyond.
try {
  ...
} catch (Exception $e) {
  // Catch almost everything?
}
What this means is that catch-all exception blocks no longer catch all exceptions (Pokemon-style), but this is a good thing, since the new exceptions aren't user generated and probably not what you expected to catch with Exception.
try {
  ...
} catch (Throwable $e) {
  // Catch everything, but is
  // that a good idea?
}

What do I do?

This assumes that you need to provide your own exception handling. Drupal 8 catches Throwables with _drupal_exception_handler, so this may be unnecessary.

set_exception_handler

If you leave your existing Pokemon handlers alone, you can use set_exception_handler much as you used set_error_handler in PHP 5.

PHP 5

Errorsset_error_handlerExceptionscatch (Exception $e) Here's how I'd do it. You can use a top level catch, of course, but the key is that you're not using set_exception_handler in PHP 5.

PHP 7

Errorsset_error_handlerExceptionsErrorset_exception_handlerExceptioncatch (Exception $e) The callback you register with set_exception_handler will presumably do similar things to set_error_handler.
try {
  $code = '<?php echo 01090; ?>';
  highlight_string($code);
} catch (ParseError $e) {
  ...
}
Rules are made to be broken, of course, and there may be times where you do have to catch errors. If you were already handling a recoverable error, you will probably have to include a catch block to match. But at least it's now close to the code.

Today

function log_exc(Exception $e) {
  ...
}

set_exception_handler('log_exc');
There's an additional wrinkle. Most of you probably have (or at least use) code something like this. There's a problem (type hint).

PHP 7 only

function log_exc(Throwable $e) {
  ...
}

set_exception_handler('log_exc');
If you're writing PHP 7 only code, then you can simply replace the type hint with Throwable.

PHP 5 and 7

function log_exc($e) {
  ...
}

set_exception_handler('log_exc');

What do I look for?

git grep set_exception_handler
git grep -E 'catch\s*\(\\?Exception\s'
End of section.

foreach

Oh no, I hear you all say. How is foreach broken? Let me count the ways…

(there are two)

These aren't that bad, and they're off in the weeds of what you'd really consider undefined behaviour, but since people probably rely on them…
$a = [1];
foreach ($a as &$v) {
  if ($v == 1) $a[] = 2;
  var_dump($v);
}

PHP 5:

int(1)
What happens when we modify an array that's being iterated over in a by reference foreach?
$a = [1];
foreach ($a as &$v) {
  if ($v == 1) $a[] = 2;
  var_dump($v);
}

PHP 7:

int(1)
int(2)
This isn't really a bad change, but it is different.

reset()key()current()end()next()prev()pos()

There is another foreach change. Who knows these functions?
$a = range(1, 3);
foreach ($a as &$v) {
  var_dump(current($a));
}

PHP 5:

int(2)
int(3)
bool(false)
Foreach used to set the internal array pointer when iterating by reference, so you could do horrible things like this.
$a = range(1, 3);
foreach ($a as &$v) {
  var_dump(current($a));
}

PHP 7:

int(1)
int(1)
int(1)
PHP 7 does not. Rejoice! REJOICE.

What do I do?

Don't use the old array iteration functions

This is probably good advice in general.

Use array_walk or array_map

array_walk($a, function (&$v, $k) {
  ...
});
$a = array_map(function ($v) {
  ...
}, $a);
If you're not actually adding or removing elements, then use a function that isolates the scope. One of PHP's most commonly reported "bugs" is loop variables causing weirdness later, and you can remove that (and simulate lexical scope).

Refactor

If you have to add or remove elements within the loop: you're going to have to figure out how not to, otherwise you're going to get inconsistent behaviour.

What do I look for?

git grep -E 'foreach.*\sas\s+&'
There are no errors. Look for any foreach by reference. Yes, all of them. (End of section.)

Hex strings are no longer numeric

function square($n) {
  return $n * $n;
}

echo square("0x10");

PHP 5:

256
Let's go back to our old friend the square function. In PHP 5, the 0x10 is treated as 16.
function square($n) {
  return $n * $n;
}

echo square("0x10");

PHP 7:

0
Note that this is silent, due to PHP's long established string→number semantics (the string is ignored from the first non-numeric-string character onwards, in this case the x). No warning.

What do I do?

$n = sscanf('0x10', '%x')[0];
echo square($n);
In terms of what you do: there are a few options. If you know it's a hex string, you can use sscanf(). Or hexdec() with some pre-processing. If you don't know, it's going to be harder. My advice is to know when you expect hex strings.

What do I look for?

¯\_(ツ)_/¯

I don't have a good grep for this, but php7mar can pick this up. Score one for tooling! (End of section.)

list() fails silently with string input

This is obscure, but I'm mentioning it because it's silent.
$s = 'abc';
list($a, $b, $c) = $s;
var_dump($a, $b, $c);

PHP 5:

string(1) "a"
string(1) "b"
string(1) "c"
Interestingly, this only worked if the right hand side was a variable, not a literal.
$s = 'abc';
list($a, $b, $c) = $s;
var_dump($a, $b, $c);

PHP 7:

NULL
NULL
NULL

What do I do?

list($a, $b, $c) = str_split($s);

What do I look for?

¯\_(ツ)_/¯

Realistically, it depends on how much you use list(). If it's not much, just check all of them to ensure they only get arrays. If it's a lot, testing. (End of section.)

yield

But we only just added that! The associativity has changed.
yield $foo or $bar;

in PHP 5:

yield ($foo or $bar);
In practice, this returns a boolean.
yield $foo or $bar;

in PHP 7:

(yield $foo) or $bar;
Accessing $bar probably isn't super useful, although this does open up the ever popular "or die" pattern.

What do I do?

Parenthesise.

What do I look for?

Really, most projects can just audit all yield statements. There's too much variation in what syntax is acceptable to come up with a sensible regex. (End of section.)

Sorting

That sounds dramatic. It's not really, though: PHP has never guaranteed a stable sort, and for performance reasons, the behaviour of PHP's sorting functions may result in different ordering for equal elements in PHP 7.
$a = ['o', 'O'];
$opt = SORT_STRING|SORT_FLAG_CASE;
sort($a, $opt);

echo json_encode($a);

in PHP 5:

["O","o"]
Note that the order gets swapped. This is what I mean by an unstable sort.
$a = ['o', 'O'];
$opt = SORT_STRING|SORT_FLAG_CASE;
sort($a, $opt);

echo json_encode($a);

in PHP 7:

["o","O"]
In PHP 7, it's more stable in this case, but the important part is that it's different.

What do I do?

This is most likely to come up in overly specific test cases. (continued on the next slide)

What do I look for?

Broken tests. I don't really see this being a problem in practice for pretty much anyone, honestly. (And if it is, run.) (End of section.)

date.timezone

PHP 5

DateTime::__construct(): It is not safe to rely on the system's timezone settings. You are *required* to use the date.timezone setting or the date_default_timezone_set() function.
...
We've all seen this in PHP 5.

PHP 7

            
It's gone! But this isn't necessarily good news; it just means PHP will choose UTC for you. You still need to set date.timezone in practice. (End of section.)
So if that's all the things that won't scream at you, what are the most likely things that will?

boolintfloatstringnullfalsetrueresourceobjectmixednumeric

These are the names that are now unavailable to classes, interfaces and traits, as they're used for scalar type declarations. This includes classes in namespaces. Remember that these names are case insensitive. I'd avoid "scalar" and "void", too.
$HTTP_RAW_POST_DATA

becomes:

file_get_contents('php://input');
You'll see an undefined variable warning there. It should be pretty obvious.

ext/mysql is gone

(really)

This obviously doesn't affect you if you're using Drupal's database functionality, but just in case…
function f() {
  global $$foo->bar;
}
You can't provide variable variables to global any more. If you did that, you were a horrible person anyway.
Although it's not really the goal of this talk, let's have a really quick look at a few new features that I think are particularly likely to be useful for Drupalistas.

Type declarations

There are two major enhancements here.

Scalar types

function foo(int $a, string $b) {
  echo gettype($a).' '.gettype($b);
}

outputs:

integer string
The cool thing is that you are guaranteed that $a and $b are those types. How that happens is configurable, though.

Strict mode

<?php
declare(strict_types=1);
include 'foo.php'; // defines foo()
foo('0', 'foo');

outputs:

Fatal error: Uncaught TypeError: Argument
1 passed to foo() must be of the type
integer, string given, called in ...
As a caller, you can control whether the inputs will be converted or not. In strict mode, the input must be the correct type.

Weak mode

<?php
include 'foo.php'; // defines foo()
foo('0', 'foo');

outputs:

integer string
This is the default. Honestly, I think this is what you'll mostly want: it behaves like PHP does currently for internal functions. The important part is that the declare() affects functions you call, not functions you define.

Return types

function add(int $a, int $b): int {
  return $a + $b;
}
You can also define return types. These too are affected by strict typing: if the return value isn't the right type in strong mode, you get a TypeError.

Return types

interface UserInterface {
  function getName(): string;
  function getEmail(): string;
  function login(string $pw): bool;
}
My feeling is that these are mostly useful in interfaces and OO, since you can now constrain what methods defined on descendants and implementors return. Which is great from a duck typing perspective! (end of section)

Null coalesce

Null coalesce

$a = isset($thing) ? $input : null;
$b = isset($stuff) ? $stuff : null;
$c = isset($brick) ? $brick : null;
We've all written code like this, I suspect.

Null coalesce

$a = $thing ?? null;
$b = $stuff ?? null;
$c = $brick ?? null;
Instead, in PHP 7, you can write this. Only one use of the variable per line! (end of section)
Space. The final frontier. Of course, to explore space, you need a... (change)

<=>

Spaceship!

Spaceship

usort($array, function ($a, $b) {
  return $a->id <=> $b->id;
});
(end of section)

And much, much more besides

Check the migration guide

Anonymous classes, expectations (better assertions), Unicode codepoint escapes, huge generator improvements (delegation, return values), Closure::call, group use...
So, when can you have all of this goodness?

SOON

SOON.

Release candidates

As of right now, we're at RC 3. The expectation is that we'll have a few more before the final (5.4 had eight!), but we're on the way, and a new RC will be released every two weeks until the white smoke appears.

Repositories

brew install php70

Ondřej Surý's DEBs

Remi Collet's RPMs

There are pre-built packages available for common distributions.
You can also build it yourself. No dependencies have changed from PHP 5 (at least on any reasonably modern distro), so if you can build 5, you can build 7.
Finally, remember that turnabout is fair play. The focus of this presentation has been helping you migrate your code, but we need your help too. If you find bugs, report them! Send PRs! Be amazing!

Questions?

https://lawngnome.github.io/php7-drupalcon/

http://php.net/migration70

@LGnome

Please rate my talk!

Image credits

Floodgates: Doug Wertman (CC-BY 2.0)

PHP7: Vincent Pontier

Panic: wackystuff (CC-BY-SA 2.0)

Don't panic: Jim Linwood (CC-BY-SA 2.0)

Head in sand: Peter (CC-BY-SA 2.0)

Flag day: Magnus Akselvoll (CC-BY 2.0)

Dual wield: Joriel Jimenez (CC-BY 2.0)

What do?: klndonnelly (CC-BY 2.0)

Strategy: Horia Varlan (CC-BY 2.0)

The Scream: Edvard Munch (PD)

Lots: swong95765 (CC-BY 2.0)

Stars: Tom Hall (CC-BY 2.0)

Sun stone: Michael McCarty (CC-BY 2.0)

Soon cat: Guyon Morée (CC-BY 2.0)

Wrenches: LadyDragonflyCC - >;< (CC-BY 2.0)

PHP 7 is (almost) here OMG! PANIC! Adam Harvey @LGnome New Relic