Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Overriding functions incompatibility

TL;DR

I want to override offsetSet($index,$value) from ArrayObject like this: offsetSet($index, MyClass $value) but it generates a fatal error ("declaration must be compatible").

What & Why

I'm trying to create an ArrayObject child-class that forces all values to be of a certain object. My plan was to do this by overriding all functions that add values and giving them a type-hint, so you cannot add anything other than values of MyClass

How

First stop: append($value);
From the SPL:

/**
 * Appends the value
 * @link http://www.php.net/manual/en/arrayobject.append.php
 * @param value mixed <p>
 * The value being appended.
 * </p>
 * @return void 
 */
public function append ($value) {}

My version:

/**
 * @param MyClass $value
 */
public function append(Myclass $value){
    parent::append($value);
}

Seems to work like a charm.

You can find and example of this working here

Second stop: offsetSet($index,$value);

Again, from the SPL:

/**
 * Sets the value at the specified index to newval
 * @link http://www.php.net/manual/en/arrayobject.offsetset.php
 * @param index mixed <p>
 * The index being set.
 * </p>
 * @param newval mixed <p>
 * The new value for the index.
 * </p>
 * @return void 
 */
public function offsetSet ($index, $newval) {}

And my version:

/**
 * @param mixed $index
 * @param Myclass $newval
 */
public function offsetSet ($index, Myclass $newval){
    parent::offsetSet($index, $newval);
}

This, however, generates the following fatal error:

Fatal error: Declaration of Namespace\MyArrayObject::offsetSet() must be compatible with that of ArrayAccess::offsetSet()

You can see a version of this NOT working here

If I define it like this, it is fine:

public function offsetSet ($index, $newval){
    parent::offsetSet($index, $newval);
}

You can see a version of this working here

Questions

  1. Why doesn't overriding offsetSet() work with above code, but append() does?
  2. Do I have all the functions that add objects if I add a definition of exchangeArray() next to those of append() and offsetSet()?
like image 916
Nanne Avatar asked Nov 27 '12 11:11

Nanne


2 Answers

abstract public void offsetSet ( mixed $offset , mixed $value )

is declared by the ArrayAccess interface while public void append ( mixed $value ) doesn't have a corresponding interface. Apparently php is more "forgiving"/lax/whatever in the latter case than with interfaces.

e.g.

<?php
class A {
    public function foo($x) { }
}

class B extends A {
    public function foo(array $x) { }
}

"only" prints a warning

Strict Standards: Declaration of B::foo() should be compatible with A::foo($x)

while

<?php
interface A {
    public function foo($x);
}

class B implements A {
    public function foo(array $x) { }
}

bails out with

Fatal error: Declaration of B::foo() must be compatible with A::foo($x)
like image 76
VolkerK Avatar answered Oct 18 '22 22:10

VolkerK


APIs should never be made more specific.

In fact, I consider it a bug that append(Myclass $value) isn't a fatal error. I consider the The fatal error on your offsetSet() as correct.

The reason for this is simple:

function f(ArrayObject $ao) { 
    $ao->append(5); //Error
} 

$ao = new YourArrayObject(); 

With an append with a type requirement, that will error. Nothing looks wrong with it though. You've effectively made the API more specific, and references to the base class are no longer able to be assumed to have the expected API.

What is basically comes down to is that if an API is made more specific, that sub class is no longer compatible with it's parent class.

This odd disparity can be seen with f: it allows you to pass a Test to it but will then fail on the $ao->append(5) execution. If a echo 'hello world'; were above it, that would execute. I consider that incorrect behavior.

In a language like C++, Java or C#, this is where generics would come into play. In PHP, I'm afraid there's not a pretty solution to this. Run time checks would be nasty and error prone, and rolling your own class would completely obliterate the advantages of having ArrayObject as the base class. Unfortunately, the desire to have ArrayObject as the base class is also the problem here. It stores mixed types, so your subclasses must store mixed types as well.

You could perhaps implement that ArrayAccess interface in your own class and clearly mark that the class is only meant to be used with a certain type of object. That would still be a bit clumsy though, I fear.

Without generics, there's not a way to have a generalized homogeneous container without runtime instanceof-style checks. The only way would be to have a ClassAArrayObject, ClassBArrayObject, etc.

like image 3
Corbin Avatar answered Oct 18 '22 20:10

Corbin