Jump to content

Transforming an object upon use?


NotionCommotion

Recommended Posts

I am using a 3rd party plugin called blamable which sets the createBy and updateBy property when entity which have these properties are changed. While it happens to by Symfony/Doctrine related, I don't think the solution need be based on either.  It is the responsibility of the developer to set the user within the blamable listener and I have seen this be done by setting it upon each request regardless of whether it will be required for the given request.  I happen to be using JWT's where a minimal user entity is stored in the token and used for most requests and I just query the DB and transform to the real user entity where needed.  As such, I don't want to make the DB call for all requests, and instead also listen for entities which contain these properties are created or updated, but listen faster than the blamable listener and set it first.  All great... Until I discover the blamable bundle also considers an entity changed when something is added or removed from a many-to-many relationship to an entity, and my listener doesn't get triggered, and updateBy is set to null resulting in a not-null constraint error.

I suppose I "could" try to predict these edge cases, but feel this is getting too fragile.  One option is to set it very early for all post/patch/put requests before I know whether it will actually be necessary, but my OCD nature doesn't want to (actually, as I write this, thinking doing so might not be so bad).

Instead, I would like to set it very early with some "preliminary user object" which gets converted to the real user object only when the blamable plugin actually tries to do something with this preliminary user object.  Below is a very condensed version of BlameableListener and AbstractTrackingListener.  I am questioning whether I am going down a rabbit hole which doesn't come back up.  Think so?  If it might be feasible, ideas where to start? Was thinking my preliminary user object might use overloading so that I know when any of the methods are called, but I don't think doing so will be allowed by an interface.  Maybe I am going about this totally the wrong way.

class BlameableListener extends AbstractTrackingListener
{
    protected $user;

    // This is the method I am suppose to set before blamable needs it.
    public function setUserValue($user)
    {
        $this->user = $user;
    }

    public function getFieldValue($meta, $field, $eventAdapter)
    {
        // Does a little work and returns $this->user.
    }

    public function prePersist(EventArgs $args)
    {
        // Checks things out and maybe calls updateField().  This is easy as I can independent listen when an entity is persisted (Doctrine talk as scheduled to be intered into the DB).
    }

    public function onFlush(EventArgs $args)
    {
        // Checks things out and maybe calls updateField().  This one is not easy as flush happens after the persist event and I have missed the chance to set the user.
    }

    protected function updateField($object, $eventAdapter, $meta, $field)
    {
        // $object is an entity which has a updateBy property which needs to be set with the logged on user
        $property = $meta->getReflectionProperty($field);
        // $property is ReflectionProperty Object ([name] => updateBy [class] => App\Entity\Asset\Asset)
        ...
        $newValue = $this->getFieldValue($meta, $field, $eventAdapter);
        // $newValue should be the user but will be NULL if I haven't set it.
        ...
        $property->setValue($object, $newValue);
    }
}

 

Link to comment
Share on other sites

1 hour ago, kicken said:

The BlameableListener only wants the username / identifier.  If you store that in your JWT, then just pass it along.  There's no need to re-generate the full user object just to pass it into the listener.

 

Thanks kicken!  Was hoping it was going to be that easy, but nothing is ever easy!

I have a TokenUser object which is my limited user object which is stored in the token.  When passing it to BlameableListener, I get error: 

The class 'App\Security\TokenUser' was not found in the chain configured namespaces App\Entity, Tbbc\MoneyBundle\Entity, Money

TokenUser contains a Ulid property, and when passing it to BlameableListener, I get error:

The class 'Symfony\Component\Uid\Ulid' was not found in the chain configured namespaces App\Entity, Tbbc\MoneyBundle\Entity, Money

So, then I tried passing BlameableListener the Ulid string value, but get error:

Blame is reference, user must be an object

I apply Blamable as an association, so I expect that explains the last error.

    #[ORM\ManyToOne(targetEntity: UserInterface::class)]
    #[ORM\JoinColumn(nullable: false)]
    #[Gedmo\Blameable(on: 'update')]
    protected ?UserInterface $updateBy = null;

I don't really understand the chain configured namespaces errors.

My condensed security.yaml is:

security:
   providers:
        app_logon_user_provider:
            id: App\Security\LogonUserProvider
        jwt:
            lexik_jwt:
                class: App\Security\TokenUser    
    firewalls:
        main:
            provider: jwt
            jwt: ~
            json_login:
                provider: app_logon_user_provider
                check_path: /authentication_token
                username_path: email
                password_path: password
                success_handler: lexik_jwt_authentication.handler.authentication_success
                failure_handler: lexik_jwt_authentication.handler.authentication_failure

TokenUser is:

final class TokenUser implements JWTUserInterface, BasicUserInterface
{
    public function __construct(private Ulid $id, private OrganizationType $type, private array $roles, private Ulid $organizationId, private ?Ulid $tenantId)
    {
    }

    public static function createFromPayload($id, array $payload):self
    {
        return new self(Ulid::fromString($id), OrganizationType::fromName($payload['type']), $payload['roles'], Ulid::fromString($payload['organizationId']), $payload['tenantId']?Ulid::fromString($payload['tenantId']):null);
    }
    
    // Some more methods...
}

My  LogonUserProvider::loadUserByIdentifier() queries the DB for the user and if it exists, returns the doctrine User.  I also tried returning TokenUser instead of the Doctrine User, but I recall it also having issues.  Maybe I should be?

My JWTCreatedListener sets the payload based on the Doctrine User, and the Jwt package takes it from there.

final class JWTCreatedListener
{
    public function onJWTCreated(JWTCreatedEvent $event)
    {
        $user = $event->getUser();
        $payload = $event->getData();

        $payload['organizationId'] = $user->getOrganization()->toRfc4122();
        $payload['tenantId'] = ($tenant=$user->getTenant())?$tenant->toRfc4122():null;
        $event->setData($payload);
     }
}

I also have another AuthenticationSuccessListener which sets some public data (never understood why this needs to be a separate listener, but oh well...).

final class AuthenticationSuccessListener
{
    public function onAuthenticationSuccessResponse(AuthenticationSuccessEvent $authenticationSuccessEvent): void
    {        
        $user = $authenticationSuccessEvent->getUser();
        if (!$user instanceof UserInterface) {
            return;
        }
        $data = $authenticationSuccessEvent->getData();
        $data['data'] = [
            'id' => $user->getId()->toRfc4122(),
            'firstName' => $user->getFirstName(),
            'lastName' => $user->getLastName(),
            'roles' => $user->getRoles(),
        ];
        $authenticationSuccessEvent->setData($data);
    }
}

The only way I have been able to eliminate errors is pass BlameableListener the hydrated Doctrine Users.  Instead of getting this Doctrine User on any POST/PUT/PATCH requests as I suggested I might do in my initial post, I am doing so during the preFlush event which I suppose is a little better.  Still kind of annoys me, and would rather not do so.

Any advise?  Thanks!

Link to comment
Share on other sites

1 hour ago, NotionCommotion said:

The only way I have been able to eliminate errors is pass BlameableListener the hydrated Doctrine Users.

I'd probably just do that if it were me.  Regenerating the user on every request isn't really going to be an issue I think, and might be necessary anyway more often than you'd think.

1 hour ago, NotionCommotion said:

When passing it to BlameableListener, I get error: 

The class 'App\Security\TokenUser' was not found in the chain configured namespaces App\Entity, Tbbc\MoneyBundle\Entity, Money

I think what this error means is that you're trying to persist the App\Security\TokenUser object, but that object does not correspond to any known entity.  Likewise for the other class.

I've never used this library so really don't know anything about it.  My initial comment was based on a quick look at the class where it seemed like it just extracted a string value from a user object. Doing a little more googling, I'm thinking maybe doctrine's reference proxies might be a solution for you.  Since I don't know how things are structured, I can't really provide a good example but whever you were passing a fully hydrated user before, you'd change to just passing a reference, something like:

$reference=$em->getReference(User::class, $jwt->getUserIdentifier());
$blamable->setUser($reference);

 

  • Like 1
Link to comment
Share on other sites

23 minutes ago, kicken said:

I'd probably just do that if it were me.  Regenerating the user on every request isn't really going to be an issue I think, and might be necessary anyway more often than you'd think.

Agree it will likely not be an issue and even more agree it will not be an issue if only occurring when the database is being changed.

25 minutes ago, kicken said:

I think what this error means is that you're trying to persist the App\Security\TokenUser object, but that object does not correspond to any known entity.  Likewise for the other class.

Figured so much.  Initially thought that if the content that was being saved was identical both times, it would somehow go through, but more I think about it, the more I think unlikely.

27 minutes ago, kicken said:

Doing a little more googling, I'm thinking maybe doctrine's reference proxies might be a solution for you.  Since I don't know how things are structured, I can't really provide a good example but whever you were passing a fully hydrated user before, you'd change to just passing a reference, something like:

$reference=$em->getReference(User::class, $jwt->getUserIdentifier());
$blamable->setUser($reference);

I've conceded on not trying to overly optimize, however, I think this is a great solution.  Let me check right now whether it works.

On face value, it works great.  The only thing I am not sure about is whether some method such as getId() is being invoked on it and it is being populated from the database.  I looked at the SQL log and found that the user table is being queried three times.  Guess if I was really concerned about performance, I wouldn't be using Doctrine.

Appreciate the help! 

Quote

If you invoke any method on the Item instance, it would fully initialize its state transparently from the database. 

 

Link to comment
Share on other sites

This thread is more than a year old. Please don't revive it unless you have something important to add.

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...

Important Information

We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.