symfonydoctrinerelationarraycollection

Prevent Doctrine from using ArrayCollection for relations


Is there a way to prevent Doctrine from using array collections when fetching relations ? The goal is to ban ORM code (includes) from the entities (Domain layer), so layers are independant, I'm playing with the clean architecture.

I have this class

class User extends AbstractModel implements UserInterface
{
    // some other fields

    /**
     * @var array|RoleInterface[]
     */
    private array $roles = [];

    /**
     * @return RoleInterface[]
     */
    final public function getRoles(): array
    {
        return (array) $this->roles;
    }

    public function hasRole(RoleInterface $role): bool
    {
        return in_array($role, $this->roles);
    }

    /**
     * @param RoleInterface[]
     * 
     * @return self
     */
    final public function setRoles(array $roles): self
    {
        $this->roles = $roles;

        return $this;
    }

    final public function addRole(RoleInterface $role): self
    {
        if (! $this->hasRole($role)) {
            $this->setRoles([...$this->roles, $role]);
        }

        return $this;
    }


    final public function removeRole(RoleInterface $role): self
    {
        if ($this->hasRole($role)) {
            $this->setRoles(array_filter(
                $this->roles, 
                fn (Role $current) => $current === $role
            ));
        }

        return $this;
    }
}

And maping is done with XML

<doctrine-mapping xmlns="https://doctrine-project.org/schemas/orm/doctrine-mapping"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
    https://raw.github.com/doctrine/doctrine2/master/doctrine-mapping.xsd">

    <entity name="App\Domain\Model\User\User">

        <!-- some other fields -->

        <many-to-many field="roles" target-entity="App\Domain\Model\Role\Role">
            <join-table name="user_role">
                <join-columns>
                    <join-column name="user_id" referenced-column-name="id" nullable="false" unique="false" />
                </join-columns>
                <inverse-join-columns>
                    <join-column name="role_id" referenced-column-name="id" nullable="false" unique="false" />
                </inverse-join-columns>
            </join-table>
        </many-to-many>
        
    </entity>

</doctrine-mapping>

The problem is when fetching an User, Doctrine tries to put an ArrayCollection in the user, which leads to en error 500. I'm aware I can remove the typing and just do $collection->toArray(), however it would mean my model complies to the ORM, and it should be the opposite.

Is there a way to configure Doctrine in such a way that it returns a native array for relations ? YML, XML or PHP will do.


Solution

  • Doctrine's using internal objects to return collection of objects (based on the Doctrine\Common\Collections\Collection interface). I'm afraid it's won't be easy to change. And actually there is no point to do so, as you're using ORM with relations and collections are an important part of it.

    In order to keep your models clean you have different options. For instance, you can have a separate model for persistence purpose on the Infrastructure layer of your application and transfer your persistence model to domain model via DTO between Infrastructure and Domain layers.

    The other option is just have a single model on Domain layer for both - persistence and domain purpose. In case of the second option, you can use $collection->toArray(), so public interface of your domain model wouldn't introduce any unnecessary dependencies.

        class User extends AbstractModel implements UserInterface
        {
            /**
             * @var Collection|RoleInterface[]
             */
            private Collection $roles;
        
            public function __construct()
            {
                $this->roles = new ArrayCollection();
            }
        
            /**
             * @return RoleInterface[]
             */
            public function getRoles(): array
            {
                return $this->roles->toArray();
            }
        }