6

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()?
Nanne
  • 64,065
  • 16
  • 119
  • 163
  • I know I can do a type-check in the function before calling the parent, but apart from that being besides the point of knowing what I'm doing wrong, it wouldn't give any user of this class an easy type-hinting to work with. – Nanne Nov 27 '12 at 11:30

2 Answers2

3

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.

Corbin
  • 33,060
  • 6
  • 68
  • 78
  • I somewhat agree with your statement, especially your example, but I need a datastructure that guarantees (and if possible, type-hints and all) it has only objects of type `MyClass`. It is trivial to just check it in a function, but that would not easily help in using/creating said object. The goal is to have an ArrayObject with only one type of object, and this seemed to be a valid option (apart from the fact that it isn't :) ). – Nanne Nov 27 '12 at 12:17
3
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)
VolkerK
  • 95,432
  • 20
  • 163
  • 226
  • hm, ofcourse. Together with @corbin 's example it means I shouldn't do it anyway. If you have a thought about how to fix my final target (something like an objectarray with only one type of variable) I'd be gratefull, but maybe I should just go back to the drawing board and/or open a second question for that :) – Nanne Nov 27 '12 at 12:22