I'm trying to make the following class compatible with native PHP serialization, specifically when running on PHP 8.1.
class SerializableDomDocument extends DOMDocument
{
private $xmlData;
public function __sleep(): array
{
$this->xmlData = $this->saveXML();
return ['xmlData'];
}
public function __wakeup(): void
{
$this->loadXML($this->xmlData);
}
}
It's all fine and dandy on lower PHP versions, but 8.1 yields Uncaught Exception: Serialization of 'SerializableDomDocument' is not allowed whenever such an object is attempted to be passed to serialize() function. Here's a sample of the code that would produce such an exception: https://3v4l.org/m8sgc.
I'm aware of the __serialize() / __unserialize() methods introduced in PHP 7.4, but using them doesn't seem to be helping either. The following piece of code results into the same exception as can be observed here: https://3v4l.org/ZU0P3.
class SerializableDomDocument extends DOMDocument
{
public function __serialize(): array
{
return ['xmlData' => $this->saveXML()];
}
public function __unserialize(array $data): void
{
$this->loadXML($data['xmlData']);
}
}
I'm quite baffled by this problem, and would really appreciate any hints. For the time being it seems like the only way forward would be to introduce an explicit normalizer/denormalizer, which would result in a breaking change in the codebase API. I'd like to avoid that.
On 10 Aug 2021, this change was commited to version 8.1 RC1:
Mark DOM classes as not serializable
So you can no longer serialize those classes.
It seems this is related to invalid methods or invalid XML content in your DOMDocument. If you do not use it, this works just fine https://3v4l.org/K91Vv
As far as I can tell php has the ability to prevent a return type from being declared where it knows it's problematic.
class Foo {
public function __clone(): Baz {
return new Baz;
}
}
class Baz {
}
$foo = new Foo;
$newFoo = clone $foo;
This results in a Fatal error: Clone method Foo::__clone() cannot declare a return type, which is perfectly sensible.
But then why would php allow things like this:
class Foo {
public function __toString(): float {
return "WAT!?!";
}
}
echo new Foo;
This results in
Fatal error: Uncaught TypeError: Return value of Foo::__toString() must be of the type float, string returned
Which doesn't make sense, because were you to try and return a float:
Fatal error: Uncaught Error: Method Foo::__toString() must return a string value
Wouldn't it make more sense for php to prevent the declared return type of these types of methods rather than give those dubious errors? If not, what is the underlying reason behind this internally? Is there some mechanical barricade that prevents php from doing this where it can do it in cases like clone?
TL;DR: Supporting type inferences on magic methods breaks backwards compatibility.
Example: what does this code output?
$foo = new Foo;
$bar = $foo->__construct();
echo get_class($bar);
If you said Foo, you are incorrect: it's Bar.
PHP has a long, complicated evolution of its return type handling.
Before PHP 7.0, return type hints were a parse error.
In PHP 7.0, we got return type declarations with very simple rules (RFC), and after perhaps the most contentious internal debate ever, we got strict types (RFC).
PHP limped along with some oddities in co- and contra-variance until PHP 7.4, where we got many of these sorted (RFC).
The behavior of today reflects this organic growth, warts and all.
You indicate that the __clone() behavior is sensible, then compare that to the apparently non-sensical __toString() behavior. I challenge that neither of them are sensible, under any rational expectation of type inference.
Here's the __clone engine code:
6642 if (ce->clone) {
6643 if (ce->clone->common.fn_flags & ZEND_ACC_STATIC) {
6644 zend_error_noreturn(E_COMPILE_ERROR, "Clone method %s::%s() cannot be static",
6645 ZSTR_VAL(ce->name), ZSTR_VAL(ce->clone->common.function_name));
6646 } else if (ce->clone->common.fn_flags & ZEND_ACC_HAS_RETURN_TYPE) {
6647 zend_error_noreturn(E_COMPILE_ERROR,
6648 "Clone method %s::%s() cannot declare a return type",
6649 ZSTR_VAL(ce->name), ZSTR_VAL(ce->clone->common.function_name));
6650 }
6651 }
Pay careful attention to that wording (emphasis mine):
Clone method ... cannot declare a return type
__clone() gave you an error not because the types were different, but because you gave a type at all! This also is a compile error:
class Foo {
public function __clone(): Foo {
return new Foo;
}
}
"Why?!", you scream.
I believe there are two reasons:
Internals is beholden to a high-bar of backwards compatibility maintenance.
Incremental improvement comes slowly, each improvement building on earlier ones.
Let's talk about #1. Consider this code, which is valid all the way back to PHP 4.0:
<?php
class Amount {
var $amount;
}
class TaxedAmount extends Amount {
var $rate;
function __toString() {
return $this->amount * $this->rate;
}
}
$item = new TaxedAmount;
$item->amount = 242.0;
$item->rate = 1.13;
echo "You owe me $" . $item->__toString() . " for this answer.";
Some poor soul used __toString as their own method, in a perfectly reasonable way. Now preserving its behavior is a top priority, so we can't make changes to the engine that break this code. That's the motivation for strict_types declaration: allowing opt-in changes to parser behavior so as to keep old behavior going while still adding new behavior.
You might ask: why don't we just fix this when declare(strict_types=1) is on? Well, because this code is perfectly valid in strict types mode, too! It even makes sense:
<?php declare(strict_types=1);
class Amount {
var $amount;
}
class TaxedAmount extends Amount {
var $rate;
function __toString(): float {
return $this->amount * $this->rate;
}
}
$item = new TaxedAmount;
$item->amount = 242.0;
$item->rate = 1.13;
echo "You owe me $" . $item->__toString() . " for this answer.";
Nothing about this code smells. It's valid PHP code. If the method were called getTotalAmount instead of __toString, no one would bat an eye. The only odd part: the method name is "reserved".
So the engine can neither (a) enforce __toString returns a string type nor (b) prevent you from setting your own return type. Because to do either would violate backwards compatibility.
What we could do, however, is implement a new affirmative opt-in that says these methods aren't directly callable. Once we do that, then we can add type inference to them. Hypothetically:
<?php declare(strict_magic=1);
class Person {
function __construct(): Person {
}
function __toString(): string {
}
// ... other magic
}
(new Person)->__construct(); // Fatal Error: cannot call magic method on strict_magic object
And this is point #2: once we have a way to protect backwards compatibility, we can add a way to enforce types on magic methods.
In summary, __construct, __destruct, __clone, __toString, etc. are both (a) functions the engine calls in certain circumstances for which it can reasonably infer types and (b) functions that - historically - can be called directly in ways that violate the reasonable type inference from (1).
This is the reason PR 4117 to fix Bug #69718 is blocked.
The only way to break this stale-mate: the developer opts in to a promise that these methods cannot be called directly. Doing that frees the engine to enforce strict type inference rules.
After upgrade to PHP 7 the logs almost choked on this kind of errors:
PHP Warning: Declaration of Example::do($a, $b, $c) should be compatible with ParentOfExample::do($c = null) in Example.php on line 22548
How do I silence these and only these errors in PHP 7?
Before PHP 7 they were E_STRICT type of warnings which could be easily dealt with. Now they're just plain old warnings. Since I do want to know about other warnings, I can't just turn off all warnings altogether.
I don't have a mental capacity to rewrite these legacy APIs not even mentioning all the software that uses them. Guess what, nobody's going to pay for that too. Neither I develop them in the first place so I'm not the one for blame. (Unit tests? Not in the fashion ten years ago.)
I would like to avoid any trickery with func_get_args and similar as much as possible.
Not really I want to downgrade to PHP 5.
I still want to know about other errors and warnings.
Is there a clean and nice way to accomplish this?
1. Workaround
Since it is not always possible to correct all the code you did not write, especially the legacy one...
if (PHP_MAJOR_VERSION >= 7) {
set_error_handler(function ($errno, $errstr) {
return strpos($errstr, 'Declaration of') === 0;
}, E_WARNING);
}
This error handler returns true for warnings beginning with Declaration of which basically tells PHP that a warning was taken care of. That's why PHP won't report this warning elsewhere.
Plus, this code will only run in PHP 7 or higher.
If you want this to happen only in regard to a specific codebase, then you could check if a file with an error belongs to that codebase or a library of interest:
if (PHP_MAJOR_VERSION >= 7) {
set_error_handler(function ($errno, $errstr, $file) {
return strpos($file, 'path/to/legacy/library') !== false &&
strpos($errstr, 'Declaration of') === 0;
}, E_WARNING);
}
2. Proper solution
As for actually fixing someone else's legacy code, there is a number of cases where this could be done between easy and manageable. In examples below class B is a subclass of A. Note that you do not necessarily will remove any LSP violations by following these examples.
Some cases are pretty easy. If in a subclass there's a missing default argument, just add it and move on. E.g. in this case:
Declaration of B::foo() should be compatible with A::foo($bar = null)
You would do:
- public function foo()
+ public function foo($bar = null)
If you have additional constrains added in a subclass, remove them from the definition, while moving inside the function's body.
Declaration of B::add(Baz $baz) should be compatible with A::add($n)
You may want to use assertions or throw an exception depending on a severity.
- public function add(Baz $baz)
+ public function add($baz)
{
+ assert($baz instanceof Baz);
If you see that the constraints are being used purely for documentation purposes, move them where they belong.
- protected function setValue(Baz $baz)
+ /**
+ * #param Baz $baz
+ */
+ protected function setValue($baz)
{
+ /** #var $baz Baz */
If you subclass has less arguments than a superclass, and you could make them optional in the superclass, just add placeholders in the subclass. Given error string:
Declaration of B::foo($param = '') should be compatible with A::foo($x = 40, $y = '')
You would do:
- public function foo($param = '')
+ public function foo($param = '', $_ = null)
If you see some arguments made required in a subclass, take the matter in your hands.
- protected function foo($bar)
+ protected function foo($bar = null)
{
+ if (empty($bar['key'])) {
+ throw new Exception("Invalid argument");
+ }
Sometimes it may be easier to alter the superclass method to exclude an optional argument altogether, falling back to func_get_args magic. Do not forget to document the missing argument.
/**
+ * #param callable $bar
*/
- public function getFoo($bar = false)
+ public function getFoo()
{
+ if (func_num_args() && $bar = func_get_arg(0)) {
+ // go on with $bar
Sure this can become very tedious if you have to remove more than one argument.
Things get much more interesting if you have serious violations of substitution principle. If you do not have typed arguments, then it is easy. Just make all extra arguments optional, then check for their presence. Given error:
Declaration of B::save($key, $value) should be compatible with A::save($foo = NULL)
You would do:
- public function save($key, $value)
+ public function save($key = null, $value = null)
{
+ if (func_num_args() < 2) {
+ throw new Exception("Required argument missing");
+ }
Note that we couldn't use func_get_args() here because it does not account for default (non-passed) arguments. We are left with only func_num_args().
If you have a whole hierarchies of classes with a diverging interface, it may be easier diverge it even further. Rename a function with conflicting definition in every class. Then add a proxy function in a single intermediary parent for these classes:
function save($arg = null) // conforms to the parent
{
$args = func_get_args();
return $this->saveExtra(...$args); // diverged interface
}
This way LSP would still be violated, although without a warning, but you get to keep all type checks you have in subclasses.
For those who want to actually correct your code so it no longer triggers the warning: I found it useful to learn that you can add additional parameters to overridden methods in subclasses as long as you give them default values. So for example, while this will trigger the warning:
//"Warning: Declaration of B::foo($arg1) should be compatible with A::foo()"
class B extends A {
function foo($arg1) {}
}
class A {
function foo() {}
}
This will not:
class B extends A {
function foo($arg1 = null) {}
}
class A {
function foo() {}
}
If you must silence the error, you can declare the class inside a silenced, immediately-invoked function expression:
<?php
// unsilenced
class Fooable {
public function foo($a, $b, $c) {}
}
// silenced
#(function () {
class ExtendedFooable extends Fooable {
public function foo($d) {}
}
})();
I would strongly recommend against this, though. It is better to fix your code than to silence warnings about how it is broken.
If you need to maintain PHP 5 compatibility, be aware that the above code only works in PHP 7, because PHP 5 did not have uniform syntax for expressions. To make it work with PHP 5, you would need to assign the function to a variable before invoking it (or make it a named function):
$_ = function () {
class ExtendedFooable extends Fooable {
public function foo($d) {}
}
};
#$_();
unset($_);
PHP 7 removes the E_STRICT error level. Info about this can be found in the PHP7 compatibility notes. You might also want to read the proposal document where it was discussed while PHP 7 was being developed.
The simple fact is this: The E_STRICT notices were introduced a number of versions ago, in an attempt to notify developers that they were using bad practice, but initially without trying to force any changes. However recent versions, and PHP 7 in particular, have become more strict about these things.
The error you're experiencing is a classic case:
You have defined a method in your class that overrides a method of the same name in the parent class, but your override method has a different argument signature.
Most modern programming languages would not actually allow this at all. PHP used to allow developers to get away with stuff like this, but the language is becoming more strict with every version, especially now with PHP 7 -- they went with a new major version number specifically so that they could justify making significant changes that break backward compatibility.
The problem you have is because you've already been ignoring the warning messages. Your question implies that this is the solution you want to continue with, but messages like "strict" and "deprecated" should be treated as an explicit warning that your code is likely to break in future versions. By ignoring them for the past number of years, you have effectively placed yourself in the situation you have now. (I know that's not what you want to hear, and doesn't really help the situation now, but it's important to make it clear)
There really isn't a work around of the kind you're looking for. The PHP language is evolving, and if you want to stick with PHP 7 your code will need to evolve too. If you really can't fix the code, then you will either have to suppress all warnings or else live with these warnings cluttering up your logs.
The other thing you need to know if you plan to stick with PHP 7 is that there are a number of other compatibility breaks with this version, including some that are quite subtle. If your code is in a state where it has errors like the one you're reporting, it means that it's probably been around for quite a while, and likely has other issues that will cause you problems in PHP 7. For code like this, I would suggest doing a more thorough audit of the code before committing to PHP 7. If you're not prepared to do that, or not prepared to fix the bugs that are found (and the implication from your question is that you are not), then I'd suggest that PHP 7 is probably an upgrade too far for you.
You do have the option of reverting to PHP 5.6. I know you said you don't want to do that, but as a short-to-medium term solution it will make things easier for you. Frankly, I think it might be your best option.
I agree: the example in the first post is bad practice.
Now what if you have that example :
class AnimalData {
public $shout;
}
class BirdData extends AnimalData {
public $wingNumber;
}
class DogData extends AnimalData {
public $legNumber;
}
class AnimalManager {
public static function displayProperties(AnimalData $animal) {
var_dump($animal->shout);
}
}
class BirdManager extends AnimalManager {
public static function displayProperties(BirdData $bird) {
self::displayProperties($bird);
var_dump($bird->wingNumber);
}
}
class DogManager extends AnimalManager {
public static function displayProperties(DogData $dog) {
self::displayProperties($dog);
var_dump($dog->legNumber);
}
}
I believe this is a legitimate code structure, nevertheless this will raise a warning in my logs because the displayProperties() do not have the same parameters. Moreover I can't make them optional by adding a = null after them...
Am I right thinking this warning is wrong in this specific example please?
I had this issue as well. I have a class that overrides a function of the parent class, but the override has different num of parameters. I can think of a few easy work arounds - but do require minor code change.
change the name of the function in the subclass (so it no longer overrides parent function)
-or-
change the parameters of the parent function, but make the extra parameters optional (e.g., function func($var1, $var2=null) - this may be easiest and require less code changes. But it may not be worth to change this in the parent if its used so many other places. So I went with #1 in my case.
If possible, instead of passing the extra params in the subclass function, use global to pull in the extra params. This is not ideal coding; but a possible band-aid anyway.
You can remove the parent class method definition altogether and intercept it with a magic method.
public function __call($name, $args)
{
if($name == 'do') {
// do things with the unknown # of args
} else {
throw new \Exception("Unknown method $name", 500);
}
}
I just ran into this problem and went this route
If the base class has fewer arguments than the derived class, it is possible to add additional argument(s) to the derived class like this:
$namespace = 'default';
if (func_num_args() > 2) {
$namespace = func_get_arg(2);
}
In this way, you add a 3rd "defaulted" argument, but do not change the signature.
I would only suggest this if you have a substantial amount of code that calls this and are unable to change that code, and want to maintain backward compatibility.
I found this situation in some old Joomla code (v1.5) where JSession::set added a $namespace param, but has JObject as a base class, where JObject::set has no such parameter.
First: I have read all of the possible duplicate posts for this, and I have looked over several sources of documentation and examples, which I have replicated in the below code. However I'm getting syntax errors when writing this in Aptana 3. Is this syntax not legal, or is it perhaps a problem with my environment?
class Story {
private $storyText;
function build () use ($storyText) {
$storyText .= "blabla";
};
}
It is a syntax error. The use statement in this form isn't allowed for a class method. It is for closures only.
I guess you want something like this:
class Story {
private $storyText;
public function build () {
$this->storyText .= "blabla";
};
}
Try to start with PHP's OOP Basics described in the manual.
I'm making a plugin system. I have a class extensionmanager that takes the name of a plugin as a constructor parameter. Long story short, this is the code I'm trying to run:
$this->parsedata = function($data) {
$this->extension::parsedata($data);
};
$this-extension is a string with the name of the plugin. I have run static functions in the exact way shown in this example before. Now I'm getting the error unexpected T_PAAMAYIM_NEKUDOTAYIM on that second line (I've heard it roughly translates to "unexpected double colon")
Could anyone help me understand why?
Before the above example I tried to run something like this
$this->parsedata = &$this->extension::parsedata;
Hence the question title. The top example I thought was closer to working so I changed it.
call_user_func may give you a solution. Somewhere in the examples you have this code :
<?php
namespace Foobar;
class Foo {
static public function test() {
print "Hello world!\n";
}
}
call_user_func(__NAMESPACE__ .'\Foo::test'); // As of PHP 5.3.0
call_user_func(array(__NAMESPACE__ .'\Foo', 'test')); // As of PHP 5.3.0
?>
I think you can easily adapt this to call your static function. For example something like :
call_user_func(array($this->extension, 'parseData'), $data);
Do that:
$self = $this;
$this->parsedata = function($data) use ($self) {
{$self->extension}::parsedata($data);
};
Yet, I would suggest to avoid static functions. After all, whoever is going to use your extension manager will need to conform to some interface. Why not take advantage of abstract methods or interfaces to make the user conform to your interface?