0
votes

I have a plugin which contains all the tables it uses.

plugins
  /MyPlugin
    /src
      /Model
        /Table
          EventsTable.php
          EventValuesTable.php
          ...

I am trying to save an Event entity and its related hasMany EventValues association.

$data = [
    'event_type_id' => 5,
    'event_values' => $values // An array of EventValue entities
];
$event = $eventsTable->newEntity($data, [
    'associated' => ['MyPlugin.EventValues']
]);

However, it cannot find the associated table and gives the following error:

Cannot marshal data for "MyPlugin" association. It is not associated with "Events"


Now I have spent hours debugging this, and here is where I am so far:

  • The exception is thrown from a new piece of code introduced to the Cake core in version 3.3

  • The exception is thrown from \Cake\ORM\Marshaller.php (Line 94) when it's trying to break down the associated array.

The Problem:

The Marshaller class seems to be treating the MyPlugin.EventValues association as a nested model association - i.e. It thinks MyPlugin is the name of a model related to the main entity being saved (Event). It's not looking for the model in my plugin's src/Table directory.

I need to find a way to get Cake to realise that MyPlugin.EventValues means:

plugins/MyPlugin/src/Model/Table/EventValuesTable.php

Instead, it's analysing it as:

src/Model/Table/MyPluginsTable.php

So my question is:

  1. Is this a Cake bug which has been introduced?
  2. If not, how can I tell it to make the association from my plugin tables?

I know the first thing you guys will ask is to see my model associations and everything, BUT this was working before updating from Cake 3.1 to 3.3, and I am 100% sure the associations are correct. Remember all the tables are in the plugin table directory, nothing is from the main app table directory.

1

1 Answers

2
votes

Association aliases are plugin-less

You seem to be mixing up classnames and association aliases, the latter can be defined independently of the class used, and possible plugin prefixes are being discarded from it.

When for example creating an association like

$this->hasMany('MyPlugin.EventValues');

the association class will set the whole alias (ie MyPlugin.EventValues) as the className option, then splits the classname (EventValues) from the plugin name (MyPlugin), and sets the former as the association name (ie EventValues).

This is effectively the same as defining the association like

$this->hasMany('EventValues', [
    'className' => 'MyPlugin.EventValues'
]);

So logically you'll now have to refer to the association via the plugin-less name, ie

'associated' => ['EventValues']

The ORM will then figure the proper classname from the plugin prefixed className option value of the association.

Non-existent associations now fail hard

What you are doing there just worked by luck in 3.1, as before 3.3, non existent associations were discarded in the marshalling process, causing things to possibly fail silently, ie ['MyPlugin.EventValues'] was effectively the same as passing an empty array.

As of 3.3, non-existent associations will cause an exception to be raised, which is what you are seeing. Quote from the docs:

Table::newEntity() and Table::patchEntity() will now raise an exception when an unknown association is in the associated key.

Cookbook > 3.X Migration Guide > 3.3 Migration Guide > ORM Improvements.

You cannot marshall entities

Marshalling entities is not supported. If you want to pass ready made entities, you cannot mark the corresponding association as to be marshalled, that would go wrong, to be exact, you would be left with an empty event_values property, as non-array values are being discarded.

So, as already mentioned, it only worked by luck. In 3.1 your non-existent association would be discarded, causing the event_values property to be excluded from the association marshalling process, resulting in the passed data to be set on the $event entity as is.

If you have ready made entites for an association, either do not mark it as to be marshalled

$event = $eventsTable->newEntity($data);

or set them on the resulting entity after the marshalling process

$event = $eventsTable->newEntity($data);
$event->event_values = $arrayOfEventValueEntities;

And of course, if there is no specific reason for creating the entities in before hand, then just pass the data in array format, and let the marshaller convert it into entities accordingly

$data = [
    'event_type_id' => 5,
    'event_values' => [
        ['event_value_property' => 'value'],
        ['event_value_property' => 'value'],
        // ...
    ]
];
$event = $eventsTable->newEntity($data, [
    'associated' => ['EventValues']
]);