Skip to content

[Known-issue] Doctrine entities could not be woven  #327

Closed

Description

@TheCelavi

Doctrine entities can not be weaved - class metadata gets all messed up.

Engine generates Entity__AopProxied, which is copy of original class, and then generates class Entity that extends Entity__AopProxied and contains weaved methods/properties.

Issue is that Entity__AopProxied contains doctrine metadata, so Doctrine actually generates such metadata that Entity__AopProxied is main class and every relation is bound to that class, not Entity.

Side effects are that, per example, table name gets suffix "__aop_proixed", while relations gets all screwed up, so any join is broken because different aliases are generated with SQL walker. Lazy loading does not work as well.

Possible solution would be (in order to preserve BC compatibility) to provide possibility to specify different method of weaving for certain classes. Per example, in this case, in order to work as it should, Entity__AopProxied should not be generated and inherited, it should be generated only Entity class.

Yes, maybe we will loose nice debugging and breakpoints, however, for now, we can sacrifice that for working code.

Activity

  1. TheCelavi commented on Jul 7, 2017

    @TheCelavi
    ContributorAuthor

    Note - I am using annotations to map entities.

  2. TheCelavi commented on Jul 7, 2017

    @TheCelavi
    ContributorAuthor

    Tried to use different mapping method - just for testing purposes, no love. Which is reasonable, Entity__AopProxied is not valid superclass....

    Here is the example of wrong SQL:

    An exception occurred while executing 'SELECT t1.id AS id_2, t1.feed_item_id AS feed_item_id_3, t1.url AS url_4, t1.platform AS platform_5, t1.views AS views_6, t1.likes AS likes_7, t1.dislikes AS dislikes_8, t1.comments AS comments_9, t1.shares AS shares_10, t1.created_at AS created_at_11, t1.updated_at AS updated_at_12, t1.created_by AS created_by_13, t1.updated_by AS updated_by_14, t1.offer_id AS offer_id_15 FROM report_item t1 WHERE t0.offer_id = ?'

    SQLSTATE[42S22]: Column not found: 1054 Unknown column 't0.offer_id' in 'where clause'").

    Note how generated alias for report_item is t1, but for where statement is t0 ? This is simple, lazy loaded relation.

  3. TheCelavi commented on Jul 8, 2017

    @TheCelavi
    ContributorAuthor

    Ok, this does not allow me to sleep at all. By playing around, going trough code, I think that there is a workaround in for this situation, however, Doctrine mapping must be modified (tried with hardcoding just to test theory, works).

    When Entity__AopProxied class is generated, its mapping data must exists and must be appropriately set. It should contain all Entity mapping data, with exception, it must be set as MappedSuperclass (see http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/inheritance-mapping.html#mapped-superclasses). In that matter Entity__AopProxied would become a mapping superclass, while generated Entity would have all required metadata as concrete class with mapping info from superclass.

    It is not just simple "replace annotation" process, Doctrine supports mapping via XML, YML, annotations and PHP...

    So, in order to do so successfully, we have to find out a certain point of time, or a method, that we can modify, intercept, whatever, so we can do this modification.

    Maybe @Ocramius, @beberlei (or someone else) could give us some pointers or advices how to do this properly...?

  4. Ocramius commented on Jul 8, 2017

    @Ocramius
  5. TheCelavi commented on Jul 8, 2017

    @TheCelavi
    ContributorAuthor

    @Ocramius not that I have that choice, either way, it seams as easy to solve/add support for doctrine entities.

    Only thing required is to modify ClassMetadata for __AopProxied entity. For what I have seen, it should be an easy pick with Doctrine's loadclassmetadata event - just to set isMappedSupperclass to true.

    I will try that approach today, I think that it will work.

  6. TheCelavi commented on Jul 8, 2017

    @TheCelavi
    ContributorAuthor

    @lisachenko Ok, got it, here is a bastebin of listener that could handle this mapping issue with Doctrine entities which are weaved: https://pastebin.com/LwdJ0hXn

    It gives a basis support, it works with my use case scenario, but in general, it is quite possible, Doctrine seams very flexible when it comes to metadata. I would create a PR for this project, an optional listener for Doctrine that can be used, if required.

    For symfony bundle, I would add a compiler pass which would determine if there is a doctrine ORM included, and if that is true, it would auto register listener.

    Feedback?

  7. added a commit that references this issue on Jul 9, 2017
  8. lisachenko commented on Jul 10, 2017

    @lisachenko
    Member

    @TheCelavi this issue is very cumbersome, yes. There is no place for double magic (goaop + doctrine), so yes, unicorns are expected )

    Just to check one more idea, could we somehow use Proxies\__CG__ namespace to make doctrine trust our generated entities? /cc @Ocramius

  9. lisachenko commented on Jul 12, 2017

    @lisachenko
    Member

    Closed via 6e2a0fe

  10. TheCelavi commented on Jul 18, 2017

    @TheCelavi
    ContributorAuthor

    If we are dealing with complex object graph with bidirectional relations and several inheritance, metadata hierarchy gets really, really complex and things tends to go bad...

    It seams that Doctrine entities could not be supported by manipulating metadata... I will have to investigate further more in regards to this topic....

    This issue ought to be open.

  11. MatthieuSarter commented on Jan 31, 2018

    @MatthieuSarter

    Hi,

    FYI, there are still some issues with the workaround. I was experiencing the same t0/t1 issue in the requests, I added the workaround, it worked fine with most of the cases, but I still got an error with some entity relationships.

    EntityA is representing sensors and EntityB is representing past sensor values. EntityB has a ManyToOne relation to EntityA, which works fine. But if I add a OneToMany relationship from EntityA to EntityB I get this error :

      [Doctrine\ORM\Mapping\MappingException]
      It is illegal to put an inverse side one-to-many or many-to-many association on mapped superclass 'Appli\Metier\Bundle\Entity\EntityB__AopProxied#pastValues'.
    

    I'm using Go AOP to add execution traces around all the methods in the Appli namespace, using this annotation in my aspect:
    @Around("execution(public|protected Appli\**->*(*))")

    If I disable this annotation, everything works fine with the OneToMany relationship between EntityA and EntityB.

  12. lisachenko commented on Jan 31, 2018

    @lisachenko
    Member

    Hi, @MatthieuSarter, if you don't need AOP logic for entities, then you could easily exclude all of them from weaving process by adding logical restriction into the pointcut:

    @Around("execution(public|protected Appli\**->*(*)) && !within(Appli\**\*Entity*)")
    

    Idea is to exclude all class entities by their namespace or Entity suffix in the name.

  13. MatthieuSarter commented on Jan 31, 2018

    @MatthieuSarter

    Thanks @lisachenko !

    It works fine this way :) And I can live with the fact of not having execution traces on the entities methods :)

  14. changed the title [-]Doctrine entities can not be weaved [/-] [+][Known-issue] Doctrine entities could not be woven [/+] on May 13, 2019
  15. lisachenko commented on Oct 22, 2026

    @lisachenko
    Member

    Re-check against 4.0 (October 2026)

    This is the oldest open issue, so it was re-verified against the current master instead of being closed on assumption. Summary: the bug described here no longer exists in 4.0. The reproduction below and the regression test in #773 cover every failure mode named in this thread.

    Root cause, for the record

    Everything reported here traces back to one thing in the 3.x extend-based engine: the proxy was a subclass of a renamed copy of the entity, Entity__AopProxied, and the mapping annotations/attributes stayed on that renamed parent. Doctrine therefore mapped the parent class:

    • table name with the __aop_proxied suffix;
    • every association bound to the parent class, not to Entity;
    • lazy-load SQL with mismatched aliases (FROM report_item t1 WHERE t0.offer_id = ? in the second comment), because the association metadata belonged to one class and the hydrated object to another.

    The mapped-superclass listener from commit 6e2a0fe (the 3.x Go\Bridge\Doctrine\MetadataLoadInterceptor, based on the pastebin in this thread) worked around it by flagging Entity__AopProxied as a mapped superclass, clearing its table and repository class, and stripping field mappings imported from traits (Gedmo Timestampable/Blameable) so they would not be mapped twice. It hit a hard limit that could not be worked around: Doctrine refuses an inverse OneToMany/ManyToMany on a mapped superclass, which is the MappingException reported by @MatthieuSarter in 2018. Complex object graphs with bidirectional relations and inheritance went bad for the same reason. #334 (woven entities missing from doctrine:schema:update) was folded into this issue in 2019.

    Why it is gone in 4.0

    The trait-based engine does not rename anything. The woven class keeps its own name, its parent, its interfaces and all of its attributes; the original method bodies move into EntityOriginalTrait, which Doctrine never maps because it is a trait. There is no second class carrying entity metadata, so there is nothing for Doctrine to get wrong, and no metadata surgery is needed. The bridge was rewritten accordingly in #671: MetadataLoadInterceptor no longer touches metadata (it only reports an intercepted mapped property when native lazy objects are off), and WovenEntityClassLocator lets the mapping driver discover entities through the autoloader, which is what fixes #334 (Doctrine's default locator require_onces the source files, bypassing the weaver and missing already-loaded entities). The setup is documented in the README under "Weaving Doctrine entities".

    What was verified

    The exact scenario from this thread, run through Doctrine ORM 3.7.5 / DBAL 4.5 against a real in-memory SQLite database with weaving active:

    • two woven entities Sensor and SensorValue with a bidirectional OneToMany (inverse, cascade: persist) / ManyToOne (owning, non-nullable join column);
    • every public method of both entities advised with execution(public ...->*(*)), i.e. the "trace everything" pointcut from the 2018 report, constructors included;
    • one column of Sensor mapped inside a trait it uses, the Gedmo case the 3.x listener special-cased;
    • entities discovered through WovenEntityClassLocator, native lazy objects enabled.

    Results:

    Check Result
    Both classes implement Go\Aop\Proxy (actually woven) yes
    Table names sensor, sensor_value, no suffix
    Association targets Sensor.values -> SensorValue, SensorValue.sensor -> Sensor
    Either side a mapped superclass no
    Trait-mapped column present in CREATE TABLE sensor, hydrated, accessor advised
    Persist with cascade, flush two INSERTs into sensor_value with the right sensor_id
    DQL SELECT s, v FROM Sensor s JOIN s.values v FROM sensor s0_ INNER JOIN sensor_value s1_ ON s0_.id = s1_.sensor_id, consistent aliases, both sides hydrated
    find(SensorValue)->getSensor() native lazy ghost of class Sensor (not a subclass), uninitialized until first access, initialized by one SELECT ... FROM sensor t0 WHERE t0.id = ?
    find(Sensor)->getValues()->count() lazy collection, one SELECT ... FROM sensor_value t0 WHERE t0.sensor_id = ?
    Advices during hydration none (Doctrine writes raw property values)
    Advices for user code every call, including __construct and addValue on cascade
    Any generated SQL mentioning aop none

    All of this is now a functional test, DoctrineBridgeTest::testDoctrineMapsAssociationsBetweenWovenEntities in #773, with the fixture script tests/Fixtures/project/bin/doctrine-associations.php. It asserts the exact SQL, so a regression of the alias problem would fail it.

    Practical consequences

    • The !within(Appli\**\*Entity*) exclusion suggested in 2018 is no longer necessary. Entities can be woven like any other class.
    • Two things remain required and are documented: discover entities through WovenEntityClassLocator, and enable native lazy objects if a mapped property is intercepted with access(...) (the proxy re-declares it with property hooks, which ORM supports only with native lazy objects). Method interception on entities needs nothing beyond the locator.
    • Doctrine's legacy generated proxies (symfony/var-exporter lazy ghosts) were not tested; they are not a dev dependency and native lazy objects are the documented mode.

    Unrelated findings made on the way

    Noted here only because they surfaced during this check; they are pointcut matching, not Doctrine:

    1. execution(public Ns\Sensor*->*(*)) matches SensorValue but not Sensor itself: NamePointcut compiles * to [^\\]+? (one or more characters), which contradicts the "any part of a name" wording in docs/pointcuts.md. Behaviour dates back to 2013.
    2. execution(public Ns\Sensor|SensorValue->*(*)) means Ns\Sensor or a global class SensorValue: the | alternation spans the whole qualified name, while the docs promise alternatives "within one name part". Either the docs or the regex should change.

    Closing this once #773 is merged.

  16. lisachenko commented on Oct 22, 2026

    @lisachenko
    Member

    Finally, I’ve closed this never-ending issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions