Search code examples
mysqldoctrine-ormtwigmany-to-manysymfony

Symfony / Doctrine ManyToMany with conditions and limit


I've created a concept of a Person model having many e-mails. I also have a Company model that can share the same e-mails with People (Person model).

In short: People OneToMany PeopleEmail ManyToOne Email (classic manytomany unidirectional with a join table)

people { id, firstname, lastname, created }
people_emails { person_id, email_id, main, created }
emails { id, email, created }

as well as:

companies { id, name, employee_count, created }
companies_emails { company_id, email_id, main, created }
emails { id, email, created }

The problem is, that I'd like to store a boolean value called "main" in the join table as follows:

person_id | email_id | main | created
    8     |     5    | true | 2014-10-21 16:54:21

...so that I can do this:

Mark Wilson (Person) has 5 emails.
3 of them are his company e-mails (shared): [email protected], [email protected], [email protected]
2 of them are his own.
1 he answers only in his leisure time
1 is his MAIN email: i.e. [email protected]

Instead of fetching all those 5 emails, I'd like to easily get the primary email just as if it was a regular Person model column:

firstname: Mark (string)
lastname: Wilson (string)
emails: (array)
primary_email: (email object)

I cannot store the "main" property anywhere else, as I want to point, that the relation between Mark and his Email is "main", not the email itself.

Now, I do have this value stored, but the problem is, how to make an entity property like:

class Person {

    (...)

    /**
     * @ORM\ManyToMany(targetEntity="Email")
     * @ORM\JoinTable(name="people_emails",
     *      joinColumns={@ORM\JoinColumn(name="person_id", referencedColumnName="id")},
     *      inverseJoinColumns={@ORM\JoinColumn(name="email_id", referencedColumnName="id", unique=true)}
     *      )
     */
    private $primary_email;

    /**
     * @ORM\ManyToMany(targetEntity="Email")
     * @ORM\JoinTable(name="people_emails",
     *      joinColumns={@ORM\JoinColumn(name="person_id", referencedColumnName="id")},
     *      inverseJoinColumns={@ORM\JoinColumn(name="email_id", referencedColumnName="id", unique=true)} // A condition?
     *      )
     */
    private $emails;

    public function getPrimaryEmail() {
        return $this->primary_email; // Single row limit?
    }

    public function getEmails() {
        return $this->emails;
    }

    (...)
}

The thing is, I would really love to have it as an entity property, just for use in any case possible without the need to write custom repostory functions for the whole Person model.

Or maybe that is a completely wrong way. I'd like to use that property in Twig like:

{{person.primary_email.email}}

To sum up: I'd like to store a ManyToMany single row relationship depending on joinTable column.

How to do that?


Solution

  • There are many ways to do this and there are also many critical things to say about your design choices. Either way here is an example of one way you could achieve this using two join tables (you need two if you want to use foreign keys).

    <?php
    
    interface AddressLink
    {
        public function getEmail();
        public function isMain();
    }
    
    
    trait EmailAddressOwner
    {
        protected abstract function getAddressLinks();
    
        public function getPrimaryEmailAddress()
        {
            return $this->getAddressLinks()->filter(function (AddressLink $addressLink) {
                return $addressLink->isMain();
            })->first();
        }
    
        public function getEmailAddresses()
        {
            return $this->getAddressLinks()->map(function (AddressLink $addressLink) {
                return $addressLink->getEmail();
            });
        }
    }
    
    class PersonEmailAddress implements AddressLink
    {
        private $person; // ManyToOne
        private $email; // ManyToOne
        private $main; // bool
    }
    
    class CompanyEmailAddress implements AddressLink
    {
        private $company; // ManyToOne
        private $email; // ManyToOne
        private $main; // bool
    }
    
    
    class Email
    {
        private $id;
        private $address;
    }
    
    class Person
    {
        /**
         * @ORM\OneToMany(targetEntity="PersonEmailAddress")
         */
        private $emailAddressLinks;
    
        use EmailAddressOwner;
    
        public function getAddressLinks()
        {
            return $this->emailAddressLinks;
        }
    
    }
    
    class Company
    {
        /**
         * @ORM\OneToMany(targetEntity="CompanyEmailAddress")
         */
        private $emailAddressLinks;
    
        use EmailAddressOwner;
    
        public function getAddressLinks()
        {
            return $this->emailAddressLinks;
        }
    }
    

    Another way would be to include one ManyToMany relation to your Email entity and one ManyToOne relation for the primary e-mail address.

    To answer you question in the comments if in twig you do

    {{ person.primaryEmail.email }}
    

    It wil actually call the getPrimaryEmail() method on your Person object. Which you can implement like I've outlined above. That way you don't need to have this extra property.