Writing custom fields to Structure object worked in K2, but doesn't in K3

Edit: Sorry for the messy thread, it took a while to figure out the actual problem. For a concise description what is the actual problem, read post #6.

Hi there,

in a controller file, I try to presort and group some events of a theater group whose page I am working on. I have successfully done something like this in Kirby 2 in the past, but I am now failing in Kirby 3 and would need some help. Here’s what I did in Kirby 2:

<?php

return function($site, $pages, $page) {

  $pastEvents = $page->events()->toStructure()->sortBy('date', 'asc')->filterBy('date', '<', time())->map(function($l) {
    $eventArray = [];
    if($l->context()->isNotEmpty()) $eventArray[] = $l->context();
    if($l->location()->isNotEmpty()) $eventArray[] = $l->location();

    $l->eventString = join(', ', $eventArray);

    return $l;
  })->groupBy('eventString')->map(function($g) {

    // here happens some date parsing, does not really matter
    // but provides the variables shown below ($rangeStrings etc.)

    $g->dateString = join("<br>", $rangeStrings);
    $g->eventString = "<br>" . $g->first()->eventString();
    $g->earliestDate = $g->first()->date('Y-m-d');

    return $g;
  })->sortBy('earliestDate', 'asc');

Note the grouping via a virtual eventString field and the creation of further virtual fields of the group object before the group is returned (sorry, if my terminology might be off, I created all this pretty much by poking in the dark, but eventually it did what I wanted it to do).

Now, when I do the same in Kirby 3, I can replicate most steps, but it fails at creating these virtual fields of the group object (which I guess is called a StructureObject internally in Kirby?). Here is an example that fails:

<?php

return function ($page) {

  $pastEvents = $page->events()->toStructure()->sortBy('date', 'asc')->filter(function ($show) {
    return $show->date()->toDate() < time();
  })->map(function($l) {
    $eventArray = [];
    if($l->event()->isNotEmpty()) $eventArray[] = $l->event();
    if($l->stage()->isNotEmpty()) $eventArray[] = $l->stage();

    $l->eventString = join(', ', $eventArray);

    return $l;
  })->groupBy('eventString')->map(function($g) {

    // again, some date parsing ...

    $g->testField = "TEST";

    return $g;
  });

At the line where I try to create the test field, it fails with this error:

Argument 1 passed to Kirby\Cms\StructureObject::setContent() must be of the type array or null, string given, called in /Users/some/path/kirby/src/Toolkit/Properties.php on line 138

Has Kirby changed in that regard? Why can’t I write strings to these groups anymore? Is there any way around it? I guess I could create mini-arrays of length 1, just to access the only value, but that would be kind of ugly, I think.

Thanks for any help!

Ok, investigating further, the single-field-array-workaround is rather cumbersome, I think.

Here is how I used to use the virtual fields in my templates in the Kirby 2 setup:

<?php foreach ($pastEvents as $event): ?>
  <p><?= $event->testField() ?></p>
<?php endforeach ?>

Now, after a lot of digging here’s the easiest way to access the testfield value of a single-field-array as described above in Kirby 3:

<?php foreach ($pastEvents as $event): ?>
  <p><?= $event->testField->content()->data()[0] ?></p>
<?php endforeach ?>

Is this intended to be like this? If so, why was the old, much easier way deprecated?
Or is this a bug? If so, I would be happy to create a github issue.

I would be very happy to have the old way back or if someone could show me alternative methods to achieve the same thing.

Thanks!

Hm, to boring of a problem?
I tried to create a minimal example of the error:

Here’s the structure field from the blueprint:

shows:
  label: Shows
  type: structure
  fields:
    city:
      label: City
      type: text
    stage:
      label: Stage
      type: text

My content file then might look like this:

Shows:

- 
  city: Berlin
  stage: Volksbühne
- 
  city: Hamburg
  stage: Kampnagel
- 
  city: Hamburg
  stage: Kampnagel

Then I try to group these shows by these data:

$myShows = $page->shows()->toStructure()->map(function($l) {

    $l->eventString = $l->stage() . ', ' . $l->city(); 
    return $l;

  })->groupBy('eventString')->map(function($g) {

    $g->testField = "TEST";
    return $g;

  });

The error occurs when I want to create the virtual test field.

Argument 1 passed to Kirby\Cms\StructureObject::setContent() must be of the type array or null, string given, called in /Users/some/path/kirby/src/Toolkit/Properties.php on line 138

If there is no solution to this, that would also be a useful info for me, then I could create a GitHub issue.

If you dump your $l and $g, you get the following:

/** 
 * The StructureObject reprents each item
 * in a Structure collection. StructureObjects
 * behave pretty much the same as Pages or Users
 * and have a Content object to access their fields.
*/
dump($l); //  Kirby\Cms\StructureObject

/**
 * The Structure class wraps
 * array data into a nicely chainable
 * collection with objects and Kirby-style
 * content with fields. The Structure class
 * is the heart and soul of our yaml conversion
 * method for pages.
 */
dump($group); // Kirby\Cms\Structure

So we are dealing with two different type of objects here.

Yes, I am aware that they are two different types of object. The first represents a line in the structure field and the second is a group. However, I am not comparing these two objects, rather I am comparing the latter object $g as it was in K2 to how it is now in K3, as it works differently (and does not work for me anymore in K3).

Ok, so here is what happens in detail in each step:

function / var Kirby 2 Kirby 3
page->shows()->toStructure() Structure (of Structure Objects) Structure (of StructureObject Objects)
map() Structure (of Structure Objects) Structure (of StructureObject Objects)
$l Structure StructureObject
groupBy() Collection (of Structure Objects) Collection (of Structure Objects)
$g Structure Structure
map() Collection (of Structure Objects) Collection (of Structure Objects)

Now, where does that leave us? Is the error maybe that the groupBy command results in a collection of Structure objects instead of a collection of StructureObject objects?

But then again, maybe that does not even matter, because what does not work for me is to write a string to a new field in the $g variable and that is simply a Structure object in both versions.

So maybe my question boils down to: Why can I not write a string to a new field in a Structure object in Kirby 3, when I could so in Kirby 2?

Ok, I think I now made sense of it all and figured out the actual problem:

Kirby 3 has changed its internal workings in a way that

  • if you use the toStructure() method, you receive a Structure object, that is in itself a collection of so called structureObject objects
  • if you use the toGroup() method you receive a Collection object, which is a collection of Structure() objects which are a collection of StructureObject objects

That all makes sense so far, I guess, they just redefined the type of objects.

The problem now is, that the Structure object apparently was changed in a way that I cannot write custom fields to it anymore (as it is just supposed to only hold StructureObject objects). And the problem with this is, that this makes it impossible for me to easily sort the groups (or Structure objects actually) within a Collection with the sortBy() method.

In Kirby 2 I could just assign a custom field to any Structure object within a collection and then sort the Structure objects in this collection by this custom field.

I think I will open an issue about this on GitHub. If somebody has any idea how to solve this another way, I am all ears.

By what criteria do you want to sort the groups?

By different ones, I guess. For example by earliest date (as in “the earliest date of any of the events within the group”). So each group consists of a collection of events. What I did in K2 was to figure out the earliest date, then write that as a custom field into the group (Structure object) and then I could just easily use this as a sort key for the sortBy function.

See the very first K2 example above, where I did just exactly that.

But even apart from sorting, it would be handy to be able to write custom fields into groups, see the first example again: I create a “dateString”, which is something like "12 – 14. Aug 2019". This date string refers to the group of events (in this example there would be three events in the group, one on the 12th, one on the 13th and one on the 14th of Aug).

Yes, I could somehow save it into the single event within the group itself, but that does not make much sense, as the string should be associated with the group. So I think Kirby 3 should still allow to write fields to Structure objects to allow just that, add data that belong to and refer to this structure.

I think I figured out, what I can do. As the groups allow me to only store stucture objects within them, I can just use a structure object to store the group data I need to carry along.

So, in my last step, I just do this:

groupBy('eventString')->map(function($g) {

    // again, some date parsing ...

    $g->groupData = [
      'earliestDate' => $myEarliestDate,
      'location' => $myLocation
    ];

    return $g;
  })

And then in my template I can access this like this:

$myGroup->groupData->earliestDate()->value();

And in the sorting step, I should be able to use a custom function to sort by this earliest date. Or is it possible to simplify this by a query?