Skip to content

Instantly share code, notes, and snippets.

@lcobucci
Last active March 5, 2017 14:00
Show Gist options
  • Save lcobucci/9e68501cfad6931119d7 to your computer and use it in GitHub Desktop.
Save lcobucci/9e68501cfad6931119d7 to your computer and use it in GitHub Desktop.
Nullable embedded objects and override attribute in Embeddeds

Scenario

As of Doctrine 2.5 we have the Embeddable feature, that allow us to separate concerns by using a separated class (with its own behaviors) inside an entity. Something like this:

<?php
/** @Embeddable */
class DateTimeInterval
{
    /** @Column(type="datetime") */
    private $begin;
    
    /** @Column(type="datetime") */
    private $end;

    /**
     * @param DateTime $begin
     * @param DateTime $end
     */
    public function __construct(DateTime $begin, DateTime $end)
    {
        $this->begin = $begin;
        $this->end = $end;
    }
    /**
     * @return DateTime
     */
    public function getStart()
    {
        return $this->start;
    }

    /**
     * @return DateTime
     */
    public function getEnd()
    {
        return $this->end;
    }

    /**
     * @param DateTime $date
     * @return boolean
     */
    public function includes(DateTime $date)
    {
        return $date >= $this->start && $date <= $this->end;
    }
}

/** @Entity */
class Event
{
    // (...) other attributes

    /**
     * The period that the event occurs
     *
     * @Embedded(class = "DateTimeInterval")
     */
    private $period;
    
    /**
     * @param DateTimeInterval $period
     */
    public function __construct(DateTimeInterval $period /*[, (...)]*/)
    {
        $this->period = $period;
        
        // (...)
    }
}

Awesome, right?

Nullable embedded objects

Sometimes I want to have an optional attribute, that reuses the same Embeddable. Something like:

<?php

/** @Entity */
class Event
{
    // (...) other attributes
    
    /**
     * The period that the event occurs
     *
     * @Embedded(class = "DateTimeInterval")
     */
    private $period;
    
    /**
     * The period that the talk submissions occurs (when the event have submissions)
     *
     * @Embedded(class = "DateTimeInterval")
     */
    private $submissionsPeriod;
    
    /**
     * @param DateTimeInterval $period
     */
    public function __construct(
        DateTimeInterval $period, 
        DateTimeInterval $submissionsPeriod = null
        /*[, (...)]*/
    ) {
        $this->period = $period;
        $this->submissionsPeriod = $submissionsPeriod;
        
        // (...)
    }
}

I know that we can have an optional association with a separated entity that have the attribute, but I believe that can be considered as a (small) over engineering.

It is "natural" to read and to know what's going on with the Event object, but won't work because both $start and $end attributes are mandatory on the DateTimeInterval. Nowadays, to have that desired behavior, we would have to create an extension of DateTimeInterval with the nullable option on the attributes annotations, but that will lead us to an empty instance of that extension class when both values were NULL.

I believe that Doctrine could allow the nullable option on Embedded annotation, so when its true all attributes would be overridden (all nullable=true). When all values were NULL Doctrine wouldn't create an empty instance, but just NULL for the whole object.

Override attributes in Embeddeds

After a small search, I saw that JPA allow the use of AttributeOverrides annotation on Embeddeds so we can make the attributes nullable. That's an interesting approach too, and we would be able to change another things (not just nullable option). Like:

<?php

/** @Entity */
class Event
{
    // (...) other attributes
    
    /**
     * The period that the event occurs
     *
     * @Embedded(class = "DateTimeInterval")
     * @AttributeOverrides({
     *      @AttributeOverride(name="begin", column= @Column(type = "date")),
     *      @AttributeOverride(name="end", column= @Column(type = "date")),
     * })
     */
    private $period;
    
    /**
     * The period that the talk submissions occurs (when the event has submissions)
     *
     * @Embedded(class = "DateTimeInterval")
     */
    private $submissionsPeriod;
    
    /**
     * @param DateTimeInterval $period
     */
    public function __construct(
        DateTimeInterval $period, 
        DateTimeInterval $submissionsPeriod = null
        /*[, (...)]*/
    ) {
        $this->period = $period;
        $this->submissionsPeriod = $submissionsPeriod;
        
        // (...)
    }
}

With this feature we will have more flexibility when using Embeddables and invert the control of the metadata.

@schmittjoh
Copy link

You can already override embeddables with AttributeOverride, but would need to add this to the class doc comment like this:

@AttributeOverrides({
    @AttributeOverride("submissionsPeriod.begin", ...)
})
class Event { ...

@lcobucci
Copy link
Author

Thanks @schmittjoh I haven't tried that 😄, but I'm working on nullable embedded

@Harrisonbro
Copy link

@Icobucci Has there been any update on this topic of nullable embeddables? Anything we can do to help push it forward?

@andy-shea
Copy link

@Harrisonbro I've submitted a pull request but given the lack of activity, I don't think it's the most sought after feature:
doctrine/orm#6151

Have a play around with it yourself and if you like it, any support for the request will be helpful in getting it merged.

@Arkemlar
Copy link

Arkemlar commented Mar 5, 2017

@schmittjoh Could you please show us working example of such override? I got Invalid field override named 'name.valueDefault' for class ... when tried to follow your tip.
name.valueDefault is a property in ORM\MappedSuperclass which has child ORM\Embeddable. I tried to override this property in my ORM\Table which uses that Embeddable.
Also I experimented a bit and made clean embeddable class with only one property, then tried to override it in entity class - same result.
@lcobucci do you succeeded in embeddable properties overrides in your case?

UPD. It seems like ClasMetadataInfo->setAttributeOverride() not even tries to inspect nested properties, so I cant understand how name.valueDefault addressing expected to be working solution.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment