Speichern einer Entity mit ManyToOne-Association resultiert in Error 500

Hallo, ich habe ein Plugin mit zwei eigenen Entities, parent und child, die über eine OneToMany-Association miteinander verbunden sind. Für ein child ist die Angabe eines parent obligatorisch.

Über ein eigenes Admin-Modul möchte ich nun zu einem vorgegebenen _parent _neue children erstellen können.

Problem: Bei meinem Code, der an die Dokumentation angelehnt ist, kommt es zum Error-500, offenbar weil im API-Call zum Speichern die ID vom gesetzten child.parent überhaupt nicht übermittelt wird (wie über die Entwicklerkonsole ersichtlich ist).

Zum Beispiel ist das parent-Objekt im Vue-Code als _this.parent _verfügbar. Nun will ich ein neues child namens  nc  erstellen, bei diesem nc.parent = this.parent setzen, und dann _nc _abspeichern. Das ergibt jedoch einen 500er Fehler:

const cr = this.repositoryFactory.create('etoile_child');            
const nc = cr.create(Shopware.Context.api);
nc.infotext = "Hello world"; // Dies wird korrekt per JSON an API übertragen,
nc.parent = this.parent; // dies nicht.
childrenRepository.save(nc, Shopware.Content.api) // --> Gibt Error 500, wahrscheinlich eben weil obligatorische parent.id im Payload fehlt.

Aber eigentlich sollte das doch funktionieren, oder? Liegt der Hund dann eher irgendwo im PHP-Teil meines Codes begraben?

 

Hier noch zum besseren Verständnis der Gesamt-Code meiner Datei page/etoile-parent-detail/index.js.

// Interessanter Teil sind die Funktionen getParent und onClickSave unten.

import template from './etoile-parent-detail.html.twig';

const { Component, Mixin } = Shopware;
const Criteria = Shopware.Data.Criteria;


Component.register('etoile-parent-detail', {
    template,

    inject: [
        'repositoryFactory'
    ],

    mixins: [
        Mixin.getByName('notification')
    ],

    metaInfo() {
        return {
            title: this.$createTitle()
        };
    },

    data() {
        console.log("data");
        return {
            parent: null,
            isLoading: false,
            processSuccess: false,
            repository: null
        };
    },

    computed: {
        columns() {
            // ...
        }
    },


    created() {
        console.log("created");
        this.repository = this.repositoryFactory.create('etoile_parent');
        this.getParent();
    },


    methods: {
        getParent() {
            // Diese Funktion lädt für this.parent die entsprechende Entity aus der Datenbank.
            this.repository
                .get(this.$route.params.id, Shopware.Context.api)
                .then((entity) => {
                    this.parent = entity;
                    console.log(Shopware);
                });
        },


        onClickSave() {
            console.log("onClickSave of detail")
            
            // Dieser Block sollen dazu dienen, ein neues child zu erstellen, ihm this.parent als parent zuzuweisen, und es dann abzuspeichern (bei jedem Klick auf 'Save', dies aber nur zu Testzwecken).
            const cr = this.repositoryFactory.create('etoile_child');            
            const nc = cr.create(Shopware.Context.api);
            nc.infotext = "Hello world"; // Dieses Feld wird korrekt an die API übertragen
            nc.parent = this.parent; // Dieses "Feld" nicht. Auch wenn nc.parentId = this.parent.id gesetzt wird, ist es nicht besser.
            cr.save(nc, Shopware.Context.api); // Ergibt besagten 500-Fehler.


            console.log(this.parent.id); // Gibt korrekte ID vom parent aus
            console.log(child.parent.id); // Gibt ebenfalls dasselbe aus.


            // Von hier an entspricht der Code dem Bundle-Example, hat mit child nichts mehr zu tun, und läuft problemlos, d.h. er speichert this.parent korrekt in der DB ab.
            this.isLoading = true;
            this.repository
                .save(this.parent, Shopware.Context.api)
                .then(() => {
                    this.getParent();
                    this.isLoading = false;
                    this.processSuccess = true;
                }).catch((exception) => {
                    this.isLoading = false;
                    this.createNotificationError({
                        title: this.$t('etoile-parent.detail.errorTitle'),
                        message: exception
                    });
                });
        },

        saveFinish() {
            console.log("saveFinish");
            this.processSuccess = false;
        }
    }
});

(Nebenbemerkung: Mit CMS-Modulen hat das alles an dieser Stelle nicht zu tun; dies erwähne ich nur, weil es bei einer anderen Frage von mir um solche ging.)

Okay, das Problem sitzt definitiv tiefer. Auch ein scheinbar korrekter API-Call führt zum 500er Fehler. Unter “detail” steht dabei: “Call to a member function getPropertyName() on null” (volle Response siehe unten). Ich habe leider überhaupt keine Ahnung, woher dieser Fehler kommt, und finde zu der Fehlermeldung auch nirgends irgendwelche Antworten. Weiß jemand, woran sie liegen könnte?

Das hier ist das JSON, mit dem ich per eine _child-_Instanz kreieren will (ein parent unter der angegebenen ID existiert bereits in der Datenbank). Ich gebe es mittels Postman direkt an die API:

{
"name":"Child kreiert per Post",
"infotext":"Textext",
"zusatz":"weitere info",
"parent":
  {"id":"8fd91fd7210641d188542a43eeaae05b"} 
}

 Das hier ist die Fehler-Antwort (gekürzt um einige weniger interessante Wiederholungen):

{
    "errors": [
        {
            "code": "0",
            "status": "500",
            "title": "Internal Server Error",
            "detail": "Call to a member function getPropertyName() on null",
            "meta": {
                "trace": [
                    {
                        "file": "/app/vendor/shopware/platform/src/Core/Framework/DataAbstractionLayer/Write/WriteCommandExtractor.php",
                        "line": 152,
                        "function": "encode",
                        "class": "Shopware\\Core\\Framework\\DataAbstractionLayer\\FieldSerializer\\ManyToOneAssociationFieldSerializer",
                        "type": "->",
                        "args": [
                            {
                                "storageName": "parent_id",
                                "referenceClass": "Etoile\\ParentStore\\Core\\Content\\Parent\\ParentDefinition",
                                "referenceDefinition": {},
                                "referenceField": "id",
                                "autoload": false,
                                "flags": [],
                                "propertyName": "parent",
                                "extensions": []
                            },
                            {},
                            {},
                            {}
                        ]
                    },
                    {
                        "file": "/app/vendor/shopware/platform/src/Core/Framework/DataAbstractionLayer/Write/WriteCommandExtractor.php",
                        "line": 82,
                        "function": "map",
                        "class": "Shopware\\Core\\Framework\\DataAbstractionLayer\\Write\\WriteCommandExtractor",
                        "type": "->",
                        "args": [
                            [
                                {
                                    "flags": [],
                                    "propertyName": "name",
                                    "extensions": []
                                },
                                {
                                    "flags": [],
                                    "propertyName": "infotext",
                                    "extensions": []
                                },
                                {
                                    "localField": "id",
                                    "referenceClass": "Etoile\\ParentStore\\Core\\Content\\Parent\\Aggregate\\ChildTranslation\\ChildTranslationDefinition",
                                    "referenceDefinition": {},
                                    "referenceField": "etoile_child_id",
                                    "autoload": false,
                                    "flags": [
                                        {}
                                    ],
                                    "propertyName": "translations",
                                    "extensions": []
                                },
                                {
                                    "storageName": "parent_id",
                                    "referenceClass": "Etoile\\ParentStore\\Core\\Content\\Parent\\ParentDefinition",
                                    "referenceDefinition": {},
                                    "referenceField": "id",
                                    "autoload": false,
                                    "flags": [],
                                    "propertyName": "parent",
                                    "extensions": []
                                },
                                {
                                    "flags": [],
                                    "propertyName": "zusatz",
                                    "extensions": []
                                },
                                {
                                    "referenceClass": "Etoile\\ParentStore\\Core\\Content\\Parent\\Aggregate\\ChildProduct\\ChildProductDefinition",
                                    "referenceDefinition": {},
                                    "referenceField": "id",
                                    "autoload": false,
                                    "flags": [],
                                    "propertyName": "products",
                                    "extensions": []
                                },
                                {
                                    "flags": [
                                        {}
                                    ],
                                    "propertyName": "createdAt",
                                    "extensions": []
                                },
                                {
                                    "flags": [],
                                    "propertyName": "updatedAt",
                                    "extensions": []
                                }
                            ],
                            {
                                "name": "Child kreiert per Post",
                                "infotext": "Textext",
                                "zusatz": "weitere info",
                                "parent": {
                                    "id": "8fd91fd7210641d188542a43eeaae05b"
                                }
                            },
                            {},
                            {}
                        ]
                    },
[gekürzt]
                    {
                        "file": "/app/vendor/shopware/platform/src/Core/Framework/Api/Controller/ApiController.php",
                        "line": 906,
                        "function": "create",
                        "class": "Shopware\\Core\\Framework\\DataAbstractionLayer\\EntityRepository",
                        "type": "->",
                        "args": [
                            [
                                {
                                    "name": "Child kreiert per Post",
                                    "infotext": "Textext",
                                    "zusatz": "weitere info",
                                    "parent": {
                                        "id": "8fd91fd7210641d188542a43eeaae05b"
                                    }
                                }
                            ],
                            {
                                "languageIdChain": [
                                    "2fbb5fe2e29a4d70aa5854ce7ce3e20b"
                                ],
                                "versionId": "0fa91ce3e96a4bc2be4bd9ce752c3425",
                                "currencyId": "b7d2554b0ce847cd82f3ac9bd1c0dfca",
                                "currencyFactor": 1,
                                "currencyPrecision": 2,
                                "scope": "user",
                                "ruleIds": [],
                                "source": {},
                                "considerInheritance": false,
                                "taxState": "gross",
                                "extensions": []
                            }
                        ]
                    },
[gekürzt]
                ],
                "file": "/app/vendor/shopware/platform/src/Core/Framework/DataAbstractionLayer/FieldSerializer/ManyToOneAssociationFieldSerializer.php",
                "line": 78
            }
        }
    ]
}

 

Setz einfach direkt das FK Field mit der ID. Vermutlich heißt es bei dir parent_id

Scheint zu funktionieren. Vielen Dank!

Ich muss nochmal hierauf zurückkommen. Jetzt habe ich es wirklich gelöst.

Das Problem saß tatsächlich tiefer: Grund war die ChildDefinition. Ich hatte in Analogie zum ManyToManyAssociationField ein ManyToOneAssociationField eingebaut (mit spiegelbildlichem OneToMannyAssociationField in der “ParentDefinition”). Jedoch war der entscheidende Fehler, dass in der ChildDefinition kein entsprechendes FkField mitdefiniert wurde. Das habe ich nun endlich durch Vergleich mit den funktionierenden TranslationDefinitions herausbekommen. (Vergleiche auch die BundleProductDefinition im Bundle-Example, die als Mittler in der ManyToMany-Assoziation fungiert.) 

Deswegen gab die API auch nach Shyims Vorschlag bei mir zunächst weiterhin (jedoch andere) Fehlermeldungen aus. Jetzt, wo das FkField definiert ist, funktioniert es tadellos sowohl nach Shyims Methode als auch mittels eines JSON mit _“parent”: { “id”:“abc123efg…” }, _wobei das Verhalten unterschiedlich ist, wenn die angegebene id in der Datenbank nicht existiert. Die Required-Flag muss dabei aufs FkField, nicht aufs ManyToOne-Field, sonst funktioniert nur die zweite Methode (vgl. diese Bemerkung).  

# ChildDefinition.php
...
new ManyToOneAssociationField('parent', 'parent_id', ParentDefinition::class, 'id', false),
(new FkField('parent_id', 'parent_id', ParentDefinition::class))->addFlags(new Required()),
...

// API-Calls per POST (Content-Type = application/json) an /api/v3/etoile-child

// funktioniert (scheitert, wenn kein parent mit angegebener id in Datenbank existiert):
{"name":"Kreiert per Methode 1", "parent_id": "4ab9ee1986c54ed486047d2f9f8c8f93"}

// funktioniert (kreiert neues parent, wenn angegebene id in Datenbank nicht existiert)
{"name":"Kreiert per Methode 2", "parent": {"id": "4ab9ee1986c54ed486047d2f9f8c8f93"}}