phpobjectinterfacearrayaccess

How does a PHP interface actually change the behavior of a class


According to PHP documentation,

Object Interfaces allow you to create code which specifies which methods a class must implement, without having to define how these methods are implemented.

Hence, an interface is like a class with predefined method which still needs to be accessed with the -> symbol

However, the ArrayAccess Interface provides accessing of objects as arrays. Making it possible for an object to be accessed with both $object->property and $object["property"]

I cannot understand how the ArrayAccess makes it possible to change an object syntax. I've written out a code to try replicating the effect of just one of the ArrayAccess method but it throws an error

// Using the PHP ArrayAccess Interface

namespace A {
    
    class myclass implements \ArrayAccess {
        public function offsetExists($offset) { return true; }
        public function offsetGet($offset) {
            // changed behaviour
            return $this->{$offset} ?? null;
        }
        public function offsetSet($offset, $value) {}
        public function offsetUnset($offset) {}
    }

    $myclass = new myclass();
    $myclass->access = 'Interface';
    echo $myclass['access']; // "Interface"

};

//Trying to implement my own ArrayAccess Interface

namespace B {
    
    interface MyArrayAccess {
        public function offsetGet($offset);
    }
    
    class myclass implements MyArrayAccess {
        public function offsetGet($offset) {
            // change behaviour
            return $this->{$offset} ?? null;
        }
    }
    
    $myclass = new myclass();
    $myclass->access = 'Interface';
    echo $myclass['access']; // Fatal error: Uncaught Error: Cannot use object of type B\myclass as array

}

Please help me explain this properly. Thanks


Solution

  • Rather than to say that interface "change the behaviour of a class", I'd say interface makes it easy to expand a class functionality.

    To understand interface, as an object-oriented programming concept, we should first understand what problem it is here to solve.

    What problem was "Interface" designed to solve?

    Interfaces are kind of contract. It is the way to achieve duck-typing in PHP. You need to think in the perspective of a library writer, who wants to expose functionality to others. For example,

    class Greeter
    {
        public function greet($person)
        {
            echo "Hello, {$person->getName()}!\n";
        }
    }
    

    To make sure that the library user knows that a $person needs to have a getName() method, you may create a class Person that has a getName() method. Then use Type Declaration to detect potential error when the code is parsed.

    class Greeter
    {
        public function greet(Person $person)
        {
            echo "Hello, {$person->getName()}!\n";
        }
    }
    
    class Person
    {
        public string $name;
        public function getName(): string
        {
            return $this->name;
        }
    }
    

    Let's assume there is another library to feed things with food:

    class Feeder {
        public function feed(Eater $eater, string $food) {
            $eater->eat($food);
        }
    }
    
    class Animal {
        private $stomach = [];
        public function eat(string $food) {
            $stomach = $food;
        }
    }
    
    

    Consider this...

    Now, let's say a user want to write a class Pet that can both eat and be greet. The user don't want to write those features again just for Pet.

    How to write Pet so both Greeter and Feeder library can be used on it?

    Perhaps something like this?

    class Pet extends Person, Animal {
    }
    

    Unfortunately, PHP DO NOT support multiple inheritance. A class can only extend one single class. The above code is not valid. So in the current situation, user can only use either one of the libraries.

    Also for different thing, "name" might be a very different concept (e.g. a person might return both $first_name and $last_name with getName()). There might not be a sensible default implementation of getName() method in your library class to begin with.

    So you, as a library writer who want his / her library to be as flexible for user as possible. What can you do?

    How to solve this problem with "Interface" in PHP?

    Interface is declaration of method signatures. It is a shortcut to declare library requirement without a concrete class / inheritance requirement.

    With interface, you may rewrite both library like so:

    Greeter library

    class Greeter {
        public function greet(Namer $namer) {
            echo "Hello, {$namer->getName()}!\n";
        }
    }
    
    interface Namer {
        public function getName(): string;
    }
    

    Feeder library

    class Feeder {
        public function feed(Eater $eater, string $food) {
            $eater->eat($food);
        }
    }
    
    interface Eater {
        public function eat(string $food);
    }
    
    

    Instead of requiring a concrete class (or parent class inheritance), a class is allowed to implement multiple interfaces. So the below Pet class is totally valid in PHP:

    class Pet implements Namer, Eater {
        private array $stomach = [];
        private string $name = '';
    
        public function __construct(string $name)
        {
            $this->name = $name;
        }
    
        /**
         * Implements Namer.
         */
        public function getName(): string
        {
            return $this->name;
        }
    
        /**
         * Implements Eater.
         */
        public function eat(string $food)
        {
            $this->stomach[] = $food;
        }
    }
    
    $greeter = new Greeter();
    $feeder = new Feeder();
    $pet = new Pet('Paul');
    
    $greeter->greet($pet);
    $feeder->feed($pet, 'a biscuit');
    

    Now, an object of this Pet class can work both with Greeter library and the Feeder library.

    What about the ArrayAccess interface?

    The ArrayAccess interface is an interface declared not by 3rd party library writers, but by core PHP writers. Core PHP writer provides even more profound support to it.

    A bit like interface we mentioned before, PHP provides feature to class that implements it. But instead of our Greeter or Feeder examples above, core PHP provides Syntactic sugar to class that implements ArrayAccess. That means you may use cleaner code to work with classes implemented AccessAccess interface.

    In the official example,

    <?php
    class Obj implements ArrayAccess {
        private $container = array();
    
        public function __construct() {
            $this->container = array(
                "one"   => 1,
                "two"   => 2,
                "three" => 3,
            );
        }
    
        public function offsetSet($offset, $value) {
            if (is_null($offset)) {
                $this->container[] = $value;
            } else {
                $this->container[$offset] = $value;
            }
        }
    
        public function offsetExists($offset) {
            return isset($this->container[$offset]);
        }
    
        public function offsetUnset($offset) {
            unset($this->container[$offset]);
        }
    
        public function offsetGet($offset) {
            return isset($this->container[$offset]) ? $this->container[$offset] : null;
        }
    }
    

    If you implemented them, then instead of these:

    $obj = new Obj;
    $obj->offsetSet(10, "hello");
    $obj->offsetSet(11, "world");
    if ($obj->offsetUnset(12)) {
        $obj->offsetUnset(12);
    }
    echo $obj->offsetGet(11);
    

    You may use $obj with array-like syntax to make your code shorter:

    $obj = new Obj;
    
    $obj[10] = "hello";
    $obj[11] = "world";
    if (isset($obj[12])) {
        unset($obj[12]);
    }
    echo $obj[11];