2

I have written a PHP library using the PHP 8.0 readonly keyword and then I realised that it would be good to support earlier versions of PHP such as 7.4.

I could easily remove the readonly keywords from my code but I don't want to do that -- they were put there for a reason!

Having a C background, I immediately thought of macros but PHP doesn't seem to have any. I've googled this answer for adding macro-like behaviour to PHP, but it looks like an overkill. And it's just for one file, and my library has 26 files at present.

Is there an easy way to make PHP 7.4 just ignore the readonly keyword and make my code cross-version? Something to the effect of this C code?

#if PHP_VERSION < 8
#define readonly /**/
#enif

Perphaps some composer build option that can pre-process files before packaging them up?

Sergey Slepov
  • 1,861
  • 13
  • 33
  • PHP does not have macros. – Markus Zeller Nov 27 '22 at 21:27
  • 1
    I assume `readonly` was added to PHP 8 because nothing similar existed before. – KIKO Software Nov 27 '22 at 21:29
  • Do you use any sort of build system / dependency manager to package your library, like composer? If so, you might be able to add a [hook script](https://getcomposer.org/doc/articles/scripts.md) to preprocess your php files when installing under php 7. – Brian61354270 Nov 27 '22 at 21:42
  • If you only set these properties in the constructor of your class, make them `private`, and don't code any other way to set them, they will virtually be `readonly` already. – KIKO Software Nov 27 '22 at 21:44
  • @Brian, yes, I do use composer, and thanks, that's a step in the right direction. I'm now reading this page to see how I can grab all source files from the hook and remove the readonly keywords: https://getcomposer.org/doc/articles/scripts.md – Sergey Slepov Nov 27 '22 at 21:48
  • 1
    Patching is not the proper way to do it, because this assumes a R/W file-system. – Martin Zeitler Nov 27 '22 at 21:50
  • @KIKOSoftware, yes, that would probably be the 7.4 way of having readonly, right? Except that I do happen to have lots of properties on my objects and having an extra getter function will at least double the amount of code. – Sergey Slepov Nov 27 '22 at 21:52
  • It's better than changing your code with Composer. Composer is bad enough as it is. – KIKO Software Nov 27 '22 at 21:56
  • PHP 7.4 end of life is tomorrow, why bother? – Alex Howansky Nov 27 '22 at 22:01
  • Alex is right, see: [Currently Supported Versions](https://www.php.net/supported-versions.php), however, [it is still very popular](https://w3techs.com/technologies/details/pl-php). – KIKO Software Nov 27 '22 at 22:06
  • You need to decide that on your own: either use features from PHP 8, or let your package support older versions of PHP. I don't think there's any easy and universally supported way to use PHP 8 features in PHP 7.4 – Nico Haase Nov 28 '22 at 10:10

3 Answers3

3

Out of the box, PHP does not include conditional compilation of the type you're hoping for.

One option would be to pre-process the source files on the fly, using a custom autoloader or Composer hook. The idea would be to let the normal code run to the point where it was going to include the file, then instead fetch its contents and manipulate it.

Note that this would not need to be a fully-functional macro system, you could just surround the code with some clear markers, like /* IF PHP 8 */ readonly /* END IF */ and match them with a simple regex pattern:

$php_code = file_get_contents($php_file_being_loaded);
if ( PHP_VERSION_ID < 80000 ) {
    $php_code = preg_replace('#/\* IF PHP 8 \*/.*?/\* END IF \*/#s', '', $php_code);
}
eval($php_code);

Alternatively, you could run the pre-processing "offline", to automatically produce parallel versions of the library: one for PHP 8.0 and above, and a different one for PHP 7.4. Again, this could be as simple as the above, or you could use a tool like Rector which parses and rewrites normal PHP code (with no extra markers) according to set rules, including "downgrading" it to be compatible with a particular version of PHP.

IMSoP
  • 89,526
  • 13
  • 117
  • 169
  • Thanks for your answer, that's more like what I was looking for! The custom autoloaders are DEPRECATED as of PHP 7.2.0, and REMOVED from PHP 8. I guess that leaves us with Composer hooks. Where would your code snippet go, which Composer hook? Could you provide a more complete example? Woud it work with a read-only filesystem? – Sergey Slepov Nov 28 '22 at 09:37
  • @SergeySlepov Sorry, that's just me not checking the link properly when I added it; what's deprecated is defining `__autoload` instead of registering a callback with `spl_autoload_register`. I've updated to a better link. That's what Composer itself is using internally anyway, as is basically every PHP application you'll use. – IMSoP Nov 28 '22 at 09:52
  • inventive! btw shouldn't that regex use ```[\s\S]?``` instead of ```.*?``` ? in most regex engines, `.` means "anything except newlines", while `[\s\S]` means anything including newlines, unless limiting the php8 blocks to single-lines is intentional? – hanshenrik Nov 28 '22 at 10:33
  • @hanshenrik To be honest, I always forget about `.` not matching newlines, thanks for the reminder. I've added the [`s` modifier](https://www.php.net/manual/en/reference.pcre.pattern.modifiers.php) which feels more natural than constructing a range containing two opposites. – IMSoP Nov 28 '22 at 10:48
  • @IMSoP, would you mind providing a more complete code sample beginning with ```spl_autoload_register(function ($class_name)...```? – Sergey Slepov Nov 28 '22 at 11:01
  • @IMSoP, and where would that code go? On top of each PHP file? Or is there a single global location where it can be included once? (sorry, I'm new to PHP) – Sergey Slepov Nov 28 '22 at 11:05
  • @SergeySlepov The code can't go inside the PHP file it's changing, it needs to be in whatever calls `include`/`require`. In pretty much all modern PHP applications, that will be an existing autoloader function, and most of the time it will be the one provided by Composer; so you would be adding it to that, or copying that logic and adding this step to it. This is probably not something you should be considering if you're new to PHP in general, though; just pick a version you want to support and code to that. – IMSoP Nov 28 '22 at 11:55
1

PHP is not being compiled, therefore there's no compiler macros.
According to Backward Incompatible Changes it's a new keyword:

readonly is a keyword now. However, it still may be used as function name.

So you have two choices: a) don't re-assign it's value or b) maintain two versions.

Martin Zeitler
  • 1
  • 19
  • 155
  • 216
  • Of course... one could as well emulate it with a setter, which only permits setting it once. – Martin Zeitler Nov 27 '22 at 21:38
  • Yes, that was my idea as well, but it is very clunky. – KIKO Software Nov 27 '22 at 21:38
  • 1
    Personally I would simply decide which PHP versions I wanted to support and adjust my code accordingly. Having no `readonly` isn't the end of the world. – KIKO Software Nov 27 '22 at 21:40
  • It worked so far without and it's probably more of a religious thing to depend on it. When packaging libraries, one can't just adjust the code, but has to define composer dependencies - so that each PHP version will get the proper library version. – Martin Zeitler Nov 27 '22 at 21:45
  • Although your conclusion is correct, your first sentence is not: PHP is compiled, and it has syntax features that are processed during that compilation. Specifically, each file is compiled when first accessed into an intermediate form ("op codes"), which is then interpreted by a simple "VM". Examples of compile-time syntax are [magic constants](https://www.php.net/manual/en/language.constants.magic.php) and [namespace aliasing](https://www.php.net/manual/en/language.namespaces.importing.php). It would not actually be *technically* difficult to implement syntax like in the question. – IMSoP Nov 27 '22 at 21:55
  • @IMSoP Most interpreted languages work like this, but I wouldn't call this compiling. There's no executable in the end. Strictly speaking Martin is correct, although I agree that some form of macros would be possible if you really wanted to have them. – KIKO Software Nov 27 '22 at 22:35
  • @KIKOSoftware Well, it's called "compiling" in the PHP source code, and every technical description of it I've ever seen. The executable is never distributed, and not normally stored to file, but it is [cached in memory to be used independent of the source](https://www.php.net/manual/en/intro.opcache.php); the line between that and Java bytecode or .Net CIL is a blurry one, the main difference being intent (op codes are not designed to be portable in any way). More importantly for the current discussion, the compile step (or whatever name you prefer) *includes pre-processing functionality*. – IMSoP Nov 27 '22 at 22:41
1

most frameworks simply avoid using modern features for this reason alone (WordPress, Symfony, Laravel), but if you insist, your best bet is probably Composer, you can have a v1.x.x with composer.json

{
    "require": {
        "php": ">=7.4"
    },
}

and a v2.x.x with composer.json

{
    "require": {
        "php": ">=8.0"
    },
}

then when people do composer require lib, composer will automatically scan for and install the newest version of your library that is compatible with the local php version and composer.json-constraints (-:

the downside is that you'll have to maintain both v1 and v2 of your library for as long as you intend to support php 7.4 though..

another option is to have a loader like lib.php

if(PHP_MAJOR_VERSION >= 8){
    require("lib_modern.php");
} else{
    require("lib_legacy.php");
}

again with the downside of having to maintain both lib_modern and lib_legacy

hanshenrik
  • 19,904
  • 4
  • 43
  • 89
  • It's probably not a good idea to use `>=` on its own in a version constraint - when PHP 9.0 comes out, both v1 and v2 in your example would claim to support it, which is probably not true. A better idea is to use `"^7.4"` and `"^8.0"`, which are short for `">=7.4 && <8.0"`, and `">=8.0 && <9.0"` respectively. – IMSoP Nov 28 '22 at 10:55
  • @IMSoP i disagree. PHP strives to maintain backwards-compatibility, most code designed for PHP8 will also run on PHP9, much like most code written for PHP7 still runs fine on PHP8. – hanshenrik Nov 28 '22 at 11:20
  • PHP only explicitly aims for backwards compatibility *within a major release*. There is [a long list of breaking changes made in 8.0](https://www.php.net/manual/en/migration80.incompatible.php), and I expect a similarly long list in 9.0. It may well be that a library happens not to hit any of those cases, but then that's fine - you can add `|| ^9.0` to the version constraint once the list is known and you've had a chance to test it. – IMSoP Nov 28 '22 at 12:01