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!
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With