3
\$\begingroup\$

I am building an application that is becoming some kind of social media platform. I need to work a lot with the entities ID's in the HTML and Javascript for the purpose of AJAX calls. However, for security reasons I don't want the original ID's exposed.

Instead of encrypting an ID every time it is displayed or being sent to the client, I figured it would be better to encrypt the ID of every entity once. When they are loaded. I tried to encrypt the $id field of entities on postLoad and decrypt them again at preUpdate. That works fine, until relations come around. Doctrine simply finds no relations because the ID of the current entity is ecrypted (duh).

I decided to not touch the $id field of the entities and create an additional field. The field will only be used when the entity is loaded and will not be persisted. Instead of adding the field and get/set methods to each entity, I decided to create an interface and abstract class. The interface is to use for checking the object later on. The abstract class is to implement the methods.

This is the interface defining the methods that are needed. I also defined getId() here. This is so I can be sure that getId() is available by checking the objects against this interface.

namespace App\Entity;

interface EncryptableEntity
{
    /**
     * @return int|null
     */
    public function getId(): ?int;

    /**
     * @return null|string
     */
    public function getEncryptedId(): ?string;

    /**
     * @param string $encryptedId
     *
     * @return EncryptableEntity
     */
    public function setEncryptedId( string $encryptedId ): EncryptableEntity;
}

This is the abstract class, implementing the field and the methods. Except the getId() method, because it is already implemented in the entity itself.

namespace App\Entity;

abstract class AbstractEncryptableEntity implements EncryptableEntity
{
    /**
     * @var null|string
     */
    private $encryptedId;

    /**
     * {@inheritdoc}
     */
    public function getEncryptedId(): ?string
    {
        return $this->encryptedId;
    }

    /**
     * {@inheritdoc}
     */
    public function setEncryptedId( string $encryptedId ): EncryptableEntity
    {
        $this->encryptedId = $encryptedId;

        return $this;
    }
}

This is an example entity extending the AbstractEncryptableEntity class.

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass="App\Repository\PostRepository")
 */
class Post extends AbstractEncryptableEntity
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="text")
     */
    private $content;

    /**
     * @ORM\ManyToOne(targetEntity="App\Entity\User", inversedBy="posts")
     * @ORM\JoinColumn(nullable=false)
     */
    private $user;

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getContent(): ?string
    {
        return $this->content;
    }

    public function setContent(string $content): self
    {
        $this->content = $content;

        return $this;
    }

    public function getUser(): ?User
    {
        return $this->user;
    }

    public function setUser(?User $user): self
    {
        $this->user = $user;

        return $this;
    }
}

The Doctrine listener class encrypts the ID of the entity and sets the field when the entity is loaded. Read more about Doctrine event listeners. I use NzoUrlEncryptor because I also use it for routing purposes. It could however be any encryption tool.

namespace App\EventListener;

use App\Entity\EncryptableEntity;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Nzo\UrlEncryptorBundle\UrlEncryptor\UrlEncryptor;

class EntityEncryptorListener
{
    private $encryptor;

    public function __construct( UrlEncryptor $encryptor )
    {
        $this->encryptor = $encryptor;
    }

    public function postLoad( LifecycleEventArgs $args )
    {
        $entity = $args->getEntity();
        if ( $entity instanceof EncryptableEntity ) {
            $entity->setEncryptedId(
                $this->encryptor->encrypt( $entity->getId() )
            );
        }
    }
}

This is the services.yml file entry.

services:

    App\EventListener\EntityEncryptorListener:
                tags:
                    - { name: doctrine.event_listener, event: postLoad }

I am quite happy with my solution. However I would have liked it even more if I didn't need the extra field. Does one of you guys have an idea about how I can omit this?

It would be best if I could somehow override Doctrine behavior so I wouldn't have to touch the entity classes at all.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.