Evolving PHP safely

in PHP3 years ago (edited)

This past week, the latest PHP RFC, Deprecate Dynamic Properties, passed 2:1. It just barely met the 2/3 vote threshold for passing, which of course can and has been spun in various pro-and-con ways. The RFC covers the change itself fairly well so I won't go into detail about it here.

The main argument people had against it was that it involves triggering deprecation warnings. Which... is kind of the point. That's kind of all it does.

This is not the first time that debate has come up, but it's been coming up more frequently and we, as PHP, need to improve the answer.

Broadly speaking, there's two prevailing arguments, both of which have merit.

Let it evolve

On the one hand, the language needs to be able to move forward. PHP is an evolving, improving language. Much of that improvement in the last few years has come from cleaning up sloppy design decisions made in the language's early days, back when "design" wasn't really a verb one would apply to the development of PHP. Others comes from catching up certain parts of the standard library with new language features that didn't exist before, but do exist now and are helpful to those features.

As a few examples:

  • In PHP 8.0, the concept of what constitutes a "numeric string" (a string that can be used as a number if you look at it the right way and squint) changed. The old definition was haphazard, inconsistent, and let to all kinds of weird bugs when you used a string as an integer or float without first making sure it made logical sense to do so. In the new definition, it's consistent across the language and much more rigorous and precise, so that "99 bottles of beer" is no longer equal to 99, and "zero" no longer equals 0.
  • In PHP 8.1, several built-in interfaces had return types added. Most dated from long before the language had return types, but now that they're available it provides more precision and compile/analysis-time checks to have them. That does necessitate classes that implement those interfaces to also have return types (they have been able to for a while, but now it becomes required). Rather than making it a hard-error, however, not having a declared return type throws a deprecation warning, and there is an attribute you can add to the method to suppress even the deprecation warning until PHP 9.0.

These are, I submit, good changes to the language. I take that as a given. They are still changes, however, so have the potential to impact (break) existing code. For that reason, most changes to PHP recently that have a potentially-breaking impact have either happened in a major version (which, per semver, is explicitly allowed at that point; that is the whole point of a major version) or have come with a deprecation warning and grace period in which they will still work the old way, but with notices that updates will be needed before the next major release, which is still several years away now.

This all seems very responsible. Changes are happening only in major releases, and there are frequently (but not always) multi-year warnings that something is going to change, with a clear migration path to prepare for it. (Usually it's a fairly simple if tedious step.) Well-behaved projects should be able to adapt in a day or less. So what's the problem?

Don't break my stuff

The main counter-argument hinges on three points:

  • Most projects these days of any size depend on a huge number of third party dependencies, thanks to the ease of use of Composer and Packagist. This is on the whole a good thing, but does have the side effect that a whole lot of code in your project is not under your control. It's usually Open Source of some kind, but even if you ignore all other barriers to contributing it's still extra friction and relying on some external party to make the adjustments.
  • A whole lot of projects have huge, ancient code bases that are not "well-behaved". They were written in the PHP 5 or even 4 era. They're large. They have mediocre tests, if any. No one at the company (or in the Open Source project today) even knows how most of it works. Tracking down all the places where numeric strings are used clumsily or where an internal interface is used without a return type is not at all a small task. Whatever smack you want to talk about those code bases, even if entirely true, they exist, there's a lot of them, and they represent a not-small amount of the PHP code out there.
  • Most automated tooling (error handlers, testing frameworks, etc.), rightly or wrongly, doesn't differentiate between an error, notice, or deprecation. If you do have a robust test suite, by default it will treat a deprecation warning about a missing return type the same as a syntax error: The test will fail. That means if you can't deploy a release unless all of your tests pass (which, to be clear, a is a very good policy), you can't deploy a release until your code is completely updated to be perfectly in line with what the syntax will be in PHP 9.0... in 3-4 years. So much for a transition period.

These are all completely valid challenges, and it's entirely valid to point them out. If we want the language to continue to evolve successfully, we need to find ways to address them.

So, two valid but contradictory positions. A far more common conundrum than most will admit. :-) What do we do with that?

Coming together

I don't have a complete answer. In fact, the purpose of this post is to ask the question.

A partial answer is to accept that treating deprecations as errors is... wrong. Legit, if your test suite is configured that way, it is wrong. If your error handler is treating deprecations the same as warnings, it is wrong. Those are bugs and it is your responsibility to fix it. (Whether it is your "fault" that the bug exists is irrelevant; it's your code, so your responsibility to fix.)

PHPUnit has already taken steps to do so. Earlier this year, PHPUnit switched to not treating deprecations as errors by default, although its generated config file still does so. Turn that off.

Error handlers also need to be improved to file deprecations to a separate channel. Some, like Symfony's or TYPO3's, already do. Others do not. That is a bug that should be fixed like any other bug.

Both of those will take time to change, though, and even longer to percolate out to all the various projects out there. That means we still need to figure out how to address these challenges while code that does it wrong is still in the wild, as it will be for years.

So... I put the question out there mainly to those who object to adding deprecations: What would you want instead? This is an entirely serious question: If using a dedicated mechanism for "this isn't broken yet, but it will change in a few years, take your time to address it" isn't what would help... what would? "Don't ever change things" is not a useful answer, but clearly the status quo is not acceptable to many either.

Would moving deprecations to an entirely different channel of some kind be useful/helpful? Something else?

Give us ideas for how to make that transition easier.

Sort:  

I think that your third point (automated tooling not differentiating) is a bit flawed, and that you have missed out entirely on an entire group of people who are complaining about the changes.

To clarify, the point myself and other library/framework authors are raising is that (a) we DO have robust testing, which (b) DOES notify us of deprecations, but (c) the pace of change means we have churn every year updating our libraries to be forwards compatible with anticipated changes as indicated by said deprecations.

Yes, we could ignore these deprecations. However, there are a few issues. First, not every consumer of our libraries will disable deprection notices, which means we get reports about them. Even if you have a policy that you do not treat deprecations as error conditions that need to be resolved, you end up having to deal with the incoming issues and patches — and the average user doesn't typically search to see if something has been submitted previously. This creates work for the maintainers, as they need to close and cross-link these reports.

Second, many of the new deprecations become fatal errors when strict types is enabled. As such, having PHPUnit not convert deprecations to exceptions doesn't fix anything for libraries that enable strict types; we still end up with new failures in our code when we test against a new PHP version, which, again, leads to work.

My point is that you can have a well-maintained library that chooses to ignore deprecations and still run into churn each year as a new PHP version is rolled out.

For projects with only one or a handful of packages, this is often not terribly onerous. But for those with more (Laminas has more than 160!), it means a substantial time investment — time where we could be dealing with reported issues or adding requested features, but instead are dealing with making our code compatible with a new minor version of PHP — when minor versions are supposed to insulate us from this very sort of problem!

Don't get me wrong: I definitely approve of the majority of changes that are happening to the language. They make it more predictable, easier to work with, and allow me to write more maintainable code. But the fact that each minor release — and don't forget, minor releases are YEARLY — leads to code churn in the libraries I maintain makes me want to give up on producing OSS, because this sort of churn is busy work with very few benefits.

Do I have any ideas on how to make things easier? Not currently. Deprecations DO allow us to adapt our code earlier to upcoming language changes, and waiting 1, 2, 3, or 4 minor versions to address them would likely require far bigger, more time-consuming changes on our part later. It's the pace at which they occur that is hard on the ecosystem.

One thought I've had is to potentially accept deprecations via an RFC, but only apply them in the final minor release of the series. This would allow libraries to address them all at once, and only once every several years. However, I suspect the process to make this happen could be quite difficult/laborious on the PHP internals developers.

Hi Matthew!

I'm curious how that's a good tradeoff. You're suggesting that (for example), dynamic properties be deprecated in 8.2, but not actually throw a deprecation (with or without the supressing attribute) until, say, 8.4, and then in 9.0 they'd error hard without the attribute?

The logistics for Internals aside for the moment, wouldn't that mean for most users they'd suddenly get hit with a massive wall of deprecations all at once? Or is that the idea, that the major library producers just take one year off every few years to deal with deprecations but can ignore them between now and then?

I suppose I can see the point, but we normally tell people to not wait longer to address issues. Would the total time invested be that different? (I really don't know; I'm not even close to being in your shoes.)

Without the deprecation notices, though, people who wanted to get a jump on the process earlier (those of us who have only single-digit libraries to keep track of), wouldn't that make it harder, because the engine isn't pointing out where we need to make updates for us?

Congratulations @crell! You have completed the following achievement on the Hive blockchain and have been rewarded with new badge(s):

You received more than 3750 upvotes.
Your next target is to reach 4000 upvotes.

You can view your badges on your board and compare yourself to others in the Ranking
If you no longer want to receive notifications, reply to this comment with the word STOP

Check out the last post from @hivebuzz:

Hive Power Up Month - Feedback from Day 21