Embed SPL object ID sometimes seems to change when second level cache is active #7269

Open
opened 2026-01-22 15:48:37 +01:00 by admin · 1 comment
Owner

Originally created by @KDederichs on GitHub (Dec 1, 2023).

Bug Report

Q A
BC Break no
Version 2.17.1

Summary

I'm building a multi tenant application based on Api-Platform and after enabling second level cache I started to see some weird behavior related to embeds in my tests (Foundry started to complain about unsaved changes)

Current behavior

Currently, Foundry complains about unsaved changes, even if there are none being made.
After looking into it it seems that Foundry by @kbond (sorry for the ping, maybe you can look over this as well :) ) is calling $om->getUnitOfWork()->getEntityChangeSet($this->object) (which is why I'm opening the issue here for now) to determine if there's changes.

After looking into what's supposed to have changed, it seems that the spl_object_id of the embed field in my object changed for some reason (I didn't touch it)

How to reproduce

Setup looks like this:

Affected entity:

#[Entity(repositoryClass: ProjectRepository::class)]
#[Cache(usage: 'NONSTRICT_READ_WRITE', region: 'write_rare')]
class Project
{
    #[Embedded(class: Address::class)]
    private Address $invoiceAddress;
}

A project context that determines the project used for the current request:

class ProjectContext
{
    private ?Project $project = null;
    private ?ProjectMember $member = null;
    private bool $isInitialized = false;
    private bool $isApiTokenRequest = false;

    public function initialize(Project $project, ProjectMember $member = null, bool $isApiTokenRequest = false): void
    {
        if ($this->isInitialized) {
            throw new \Exception('Project Context already initialized');
        }
        $this->isInitialized = true;
        $this->project = $project;
        $this->member = $member;
        $this->isApiTokenRequest = $isApiTokenRequest;

        // "Fixes" delete changeset error
        assert($this->project->getInvoiceAddress() instanceof Address);
    }

    public function getCurrentProject(): Project
    {
        if (null === $this->project) {
            throw new ProjectNotSetException('No Project yet!');
        }

        return $this->project;
    }
}

A listener that sets the project:

#[AsEventListener(event: 'kernel.request', method: 'onKernelRequest', priority: 7)]
readonly class ProjectListener
{
    public function __construct(
        private ProjectContext $projectContext,
        private ProjectResolver $projectResolver,
        private ProjectMemberResolver $memberResolver,
        private Security $security
    ) {
    }

    public function onKernelRequest(RequestEvent $event): void
    {
        $request = $event->getRequest();
        $uri = $request->getRequestUri();
        $user = $this->security->getUser();
        if (!$event->isMainRequest() || !str_starts_with($uri, '/api') || '/api/docs' === $uri || null === $user) {
            // don't do anything if it's not the main request
            return;
        }

        try {
            $project = $this->projectResolver->resolve($request);
            $isApiRequest = $request->headers->has(ProjectAuthenticator::HEADER_NAME);
            $projectMember = null;
            if (!$isApiRequest) {
                $projectMember = $this->memberResolver->resolve($project);
            }
            $this->projectContext->initialize($project, $projectMember, $isApiRequest);
        } catch (ProjectNotFoundException) {
        } catch (MemberNotFoundException) {
        }
    }
}

The issue is I can't exactly figure out WHY the entity has a changed embed so I can't really tell how to reproduce it (yet, I'm still trying to find it but in the meantime I thought I already mention this because maybe someone can point me in the right direction) but adding assert($this->project->getInvoiceAddress() instanceof Address); or better generally accessing the embed there "fixes" one of the test errors (but some others still remain) which is a bit strange in itself.

Anyone got any idea what's up with this? Or has some pointers for me to dig deeper?

Expected behavior

It correctly detects there are no changes.

Originally created by @KDederichs on GitHub (Dec 1, 2023). ### Bug Report <!-- Fill in the relevant information below to help triage your issue. --> | Q | A |------------ | ------ | BC Break | no | Version | 2.17.1 #### Summary I'm building a multi tenant application based on Api-Platform and after enabling second level cache I started to see some weird behavior related to embeds in my tests (Foundry started to complain about unsaved changes) #### Current behavior Currently, Foundry complains about unsaved changes, even if there are none being made. After looking into it it seems that Foundry by @kbond (sorry for the ping, maybe you can look over this as well :) ) is calling `$om->getUnitOfWork()->getEntityChangeSet($this->object)` (which is why I'm opening the issue here for now) to determine if there's changes. After looking into what's supposed to have changed, it seems that the `spl_object_id` of the embed field in my object changed for some reason (I didn't touch it) #### How to reproduce Setup looks like this: Affected entity: ```php #[Entity(repositoryClass: ProjectRepository::class)] #[Cache(usage: 'NONSTRICT_READ_WRITE', region: 'write_rare')] class Project { #[Embedded(class: Address::class)] private Address $invoiceAddress; } ``` A project context that determines the project used for the current request: ```php class ProjectContext { private ?Project $project = null; private ?ProjectMember $member = null; private bool $isInitialized = false; private bool $isApiTokenRequest = false; public function initialize(Project $project, ProjectMember $member = null, bool $isApiTokenRequest = false): void { if ($this->isInitialized) { throw new \Exception('Project Context already initialized'); } $this->isInitialized = true; $this->project = $project; $this->member = $member; $this->isApiTokenRequest = $isApiTokenRequest; // "Fixes" delete changeset error assert($this->project->getInvoiceAddress() instanceof Address); } public function getCurrentProject(): Project { if (null === $this->project) { throw new ProjectNotSetException('No Project yet!'); } return $this->project; } } ``` A listener that sets the project: ```php #[AsEventListener(event: 'kernel.request', method: 'onKernelRequest', priority: 7)] readonly class ProjectListener { public function __construct( private ProjectContext $projectContext, private ProjectResolver $projectResolver, private ProjectMemberResolver $memberResolver, private Security $security ) { } public function onKernelRequest(RequestEvent $event): void { $request = $event->getRequest(); $uri = $request->getRequestUri(); $user = $this->security->getUser(); if (!$event->isMainRequest() || !str_starts_with($uri, '/api') || '/api/docs' === $uri || null === $user) { // don't do anything if it's not the main request return; } try { $project = $this->projectResolver->resolve($request); $isApiRequest = $request->headers->has(ProjectAuthenticator::HEADER_NAME); $projectMember = null; if (!$isApiRequest) { $projectMember = $this->memberResolver->resolve($project); } $this->projectContext->initialize($project, $projectMember, $isApiRequest); } catch (ProjectNotFoundException) { } catch (MemberNotFoundException) { } } } ``` The issue is I can't exactly figure out WHY the entity has a changed embed so I can't really tell how to reproduce it (yet, I'm still trying to find it but in the meantime I thought I already mention this because maybe someone can point me in the right direction) but adding `assert($this->project->getInvoiceAddress() instanceof Address);` or better generally accessing the embed there "fixes" one of the test errors (but some others still remain) which is a bit strange in itself. Anyone got any idea what's up with this? Or has some pointers for me to dig deeper? #### Expected behavior It correctly detects there are no changes.
Author
Owner

@KDederichs commented on GitHub (Dec 1, 2023):

Dug deeper and managed to make a reproducer https://github.com/KDederichs/embed_spl_reproducer , doesn't seem to be related to the context at all since it already happens with just Foundry stuff.

Just composer up -d and run the one test that's in there

@KDederichs commented on GitHub (Dec 1, 2023): Dug deeper and managed to make a reproducer https://github.com/KDederichs/embed_spl_reproducer , doesn't seem to be related to the context at all since it already happens with just Foundry stuff. Just composer up -d and run the one test that's in there
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: doctrine/archived-orm#7269