Search code examples
phpstrong-typing

PHP typed return mixed content


I have, as a good practice, started to turn my code to typed properties/typed function returns. But I have a question about mixed content.

I know it's not usually a good practice to return mixed content, but I have a situation where it is unavoidable. I have a class that I use as a container of data (it has multiple functionalities but I'll skip to the basics):

class Container {
  private array $list = [];

  public set(string $key, ?? $value): void {
    $this->list[$key] = $value;
  }

  public get(string $key): ?? {
    return $this->list[$key];
  }
}

I have skipped all the checks and all the other functionalities. The point is that list can hold any kind of stuff (int, boolean, string, other class objects...) so how should I approach this?

I know that PHP 8 will have union return types so at least I will be able to narrow it down (int|float|string...) but as this is quite new I don't know what should I do.


Solution

  • To keep it short. You can 't do that with PHP7. As you said already, union type declarations will be available in PHP8, which is not yet released on the current day.

    An alternative could be writing collections for every type you are working with. You could work with interfaces to identify a type. In a perfect world everything is an object like in the following example.

    First the collections.

    <?php
    declare(strict_types=1);
    namespace YourApp\Model;
    
    use InvalidArgumentException;
    use SplObjectStorage;
    
    class TeamCollection extends SplObjectStorage
    {
        public function attach(object $object, $data = null): void
        {
            if ($object instanceof Team) {
                throw new InvalidArgumentException(sprintf(
                    'The given object is not a team. %s given.',
                    get_class($object)
                ));
            }
    
            parent::attach($object, $data);
        }
    }
    

    This is a collection for Team classes. It only accepts Team classes when attaching something to this collection. Otherwise it would throw an exception. Because of inheritation you can not type hint the object to Team, because this would throw another error. The SplObjectCollection class defines the $object parameter as object type. You can not overwrite it. Therefore we can check in the attach method if a Team class is given.

    <?php
    declare(strict_types=1);
    namespace YourApp\Model;
    
    use YourApp\Model\TeamMemberCollection;
    use YourApp\Model\TeamMember;
    
    interface Team
    {
        public function getName(): ?string;
        public function setName(string $name): self;
        public function addMember(TeamMember $member): self
        public function getMembers(): TeamMemberCollection;
    }
    

    The interface defines the methods we need for a team. Now we can write a Team class.

    <?php
    declare(strict_types=1);
    namespace YourApp\Model;
    
    class SoccerTeam implements Team
    {
        protected TeamMemberCollection $members;
        protected string $name;
    
        public function __construct()
        {
            $this->members = new TeamMemberCollection();
        }
    
        public function getName(): ?string
        {
            return $this->name;
        }
    
        public function setName(string $name): self
        {
            $this->name = $name;
            return $this;
        }
    
        public function addTeamMember(TeamMember $member): self
        {
            $this->members->attach($member);
            return $this;
        }
    
        public function getMembers(): TeamMemberCollection
        {
            return $this->members;
        }
    }
    

    This example of a team class can be transferred to the TeamMember class. In principle, it would go exactly like this.

    Now let us have a look on our team collection and how it works.

    $teams = new TeamCollection();
    
    $team = (new SoccerTeam())
        ->setName('Borussia Dortmund');
    
    $teams->attach($team);
    

    This is a positive example. Because of the SoccerTeam class implements the Team interface, it would be accepted by the team collection. The collection itself checks if a Team instance is attached. Any other instances would result in an exception.