Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

doctrine2 extra lazy fetching of association

I have a Thread entity which has a OneToMany association with a Message entity. I am fetching a thread with a DQL query, and I want to limit its amount of messages to 10. Therefore I am setting the fetch mode to EXTRA_LAZY as below.

class Thread
{
    // ...

    /**
     * @var ArrayCollection
     * @ORM\OneToMany(targetEntity="Profile\Entity\Message", mappedBy="thread", fetch="EXTRA_LAZY")
     * @ORM\OrderBy({"timeSent" = "ASC"})
     */
    protected $messages;
}

This allows me to use the slice method to issue a LIMIT SQL query to the database. All good so far. Because my messages are encrypted, I need to decrypt them in my service layer before handling the thread object off to the controller (and ultimately view). To accomplish this, I am doing the following in my service:

foreach ($thread->getMessages()->slice(0, 10) as $message) {
    // Decrypt message
}

The call to slice triggers an SQL query that fetches 10 messages. In my view, I am doing the following to render the thread's messages:

$this->partialLoop()->setObjectKey('message');
echo $this->partialLoop('partial/thread/message.phtml', $thread->getMessages());

The problem is that this fetches the entire collection of messages from the database. If I call slice as in my service, the same SQL query with LIMIT 10 is issued to the database, which is not desirable.

How can I process a limited collection of messages in my service layer without issuing another SQL query in my view? That is, to have doctrine create a single SQL query, not two. I could simply decrypt my messages in my view, but that kind of defeats the purpose of having a service layer in this case. I could surely fetch the messages "manually" and add them to the thread object, but if I could do it automatically through the association, then that would be much preferred.

Thanks in advance!

like image 281
ba0708 Avatar asked Dec 26 '22 07:12

ba0708


1 Answers

How about a slightly different approach than most have suggested:

Slice

In the Thread entity, have a dedicated method for returning slices of messages:

class Thread
{
    // ...

    /**
     * @param  int      $offset
     * @param  int|null $length
     * @return array
     */
    public function getSliceOfMessages($offset, $length = null)
    {
        return $this->messages->slice($offset, $length);
    }
}

This would take care of easily retrieving a slice in the view, without the risk of fetching the entire collection.

Decrypting message content

Next you need the decrypted content of the messages.

I suggest you create a service that can handle encryption/decryption, and have the Message entity depend on it.

class Message
{
    // ...

    /**
     * @var CryptService
     */
    protected $crypt;

    /**
     * @param CryptService $crypt
     */
    public function __construct(CryptService $crypt)
    {
        $this->crypt = $crypt;
    }
}

Now you have to create Message entities by passing a CryptService to it. You can manage that in the service that creates Message entities.

But this will only take care of Message entities that you instantiate, not the ones Doctrine instantiates. For this, you can use the PostLoad event.

Create an event-listener:

class SetCryptServiceOnMessageListener
{
    /**
     * @var CryptService
     */
    protected $crypt;

    /**
     * @param CryptService $crypt
     */
    public function __construct(CryptService $crypt)
    {
        $this->crypt = $crypt;
    }

    /**
     * @param LifecycleEventArgs $event
     */
    public function postLoad(LifecycleEventArgs $event)
    {
        $entity = $args->getObject();

        if ($entity instanceof Message) {
            $message->setCryptService($this->crypt);
        }
    }
}

This event-listener will inject a CryptService into the Message entity whenever Doctrine loads one.

Register the event-listener in the bootstrap/configuration phase of your application:

$eventListener = new SetCryptServiceOnMessageListener($crypt);
$eventManager  = $entityManager->getEventManager();
$eventManager->addEventListener(array(Events::postLoad), $eventListener);

Add the setter to the Message entity:

class Message
{
    // ...

    /**
     * @param CryptService $crypt
     */
    public function setCryptService(CryptService $crypt)
    {
        if ($this->crypt !== null) {
            throw new \RuntimeException('We already have a crypt service, you cannot swap it.');
        }

        $this->crypt = $crypt;
    }
}

As you can see, the setter safeguards against swapping out the CryptService (you only need to set it when none is present).

Now a Message entity will always have a CryptService as dependency, whether you or Doctrine instantiated it!

Finally we can use the CryptService to encrypt and decrypt the content:

class Message
{
    // ...

    /**
     * @param string $content
     */
    public function setContent($content)
    {
        $this->content = $this->crypt->encrypt($content);
    }     

    /**
     * @return string
     */
    public function getContent()
    {
        return $this->crypt->decrypt($this->content);
    }
}

Usage

In the view you can do something like this:

foreach ($thread->getSliceOfMessages(0, 10) as $message) {
    echo $message->getContent();
}

As you can see this is dead simple!

Another pro is that the content can only exist in encrypted form in a Message entity. You can never accidentally store unencrypted content in the database.

like image 81
Jasper N. Brouwer Avatar answered Jan 04 '23 18:01

Jasper N. Brouwer