Using hooks to write content elsewhere in site?

Hi all,

I’m currently working on a site with vaguely this structure:

content/people/1-john-doe/person.txt
content/people/2-jane-doe/person.txt
content/events/1-disco/event.txt
content/events/2-lecture/event.txt

I’m wondering if there’s a simple way using hooks to update any person.txt while editing any event.txt in the panel. Think of it like this: on the page for 2-lecture I list that 1-john-doe was at the event. I do this with a page panel field that updates adds page: people/john-doe to 2-lecture/event.txt, so that it looks like this:

Title: Lecture
----
In-attendance: 
–
  person: people/john-doe

which works great. But when I do that, I also want to edit 1-john-doe/person.txt to do basically the inverse and list that they took part in this event. The end goal is for person.txt to look like this:

Title: John Doe
----
Attended-events:
—
  event: events/lecture

and then any time a new event is created and a person is listed as having attended it would update their .txt file over time to eventually be something like:

Title: John Doe
----
Attended-events:
—
  event: events/lecture
—
  event: events/party
—
  event: events/skygazing

the only way I can think to do this right now is to have a hook on panel.page.update like so:

kirby()->hook('panel.page.update', function($page, $oldPage = null) {

    //Check if there are any attendees listed
    if($page->attendees()->isNotEmpty()):

        foreach($page->attendees()->toStructure() as $attendee):

            // Since we have to write to the correct URL and sometimes they have indexes:
            $personUrl = $attendee->person()->value();
            $personIndex = site()->find('people')->children()->indexOf($personUrl)+1;
            $personFolder = $personIndex . '-' . explode('/' $personUrl)[1];
            // /content/people/john-doe   becomes   /content/people/1-john-doe
            $personPath = kirby()->roots()->content() . '/people/' . $personFolder;

            //create the string to append to the end of person.txt:
           $thisEvent = "-\r\n    event: " . $page->uri();
            
            //append it
            f::write( $personPath . '/person.txt', $thisEvent, true);

        endforeach;
    endif;

Which, basically, finds the correct folder through a slightly messy process of adding the indexes back into the URI, then appends:

-
  event: events/*EVENT-PAGE-BEING-EDITED-IN-PANEL*

to the bottom of it.

This does work, however it seems precarious seeing as how in order for this to work the “Attended-events” section will have to remain at the bottom of each person.txt file permanently.

Which gets me to my question:

This idea i’ve concocted—manually adding content to a field at the end of a file—works, but seems incredibly harebrained and I feel confident that there is a faster/safer/any other way to approach something like this that might already be built in. Is there?

Yeah, that looks a bit complicated. You can just update a page with $page->update(). Here’s a function that adds an item to a structure field:

    /**
     * Add a new element to a kirby structure field
     * @param string $page
     * @param string $field
     * @param array $data
     */
    function addToStructure($page, $field, $data = array()){
      $fieldData = page($page)->$field()->yaml();
      $fieldData[] = $data;
      $fieldData = yaml::encode($fieldData);
      try {
        page($page)->update(array($field => $fieldData));
        return true;
      } catch(Exception $e) {
        return $e->getMessage();
      }
    }

Another way of getting the data, would be to fetch all attended events for a person at runtime without prior writing all the events to the person’s file, i.e. for each person, loop through the events folder and grab all the events that a person has attended.

<?php

function getAttendedEvents($eventsPage, $structureField, $fieldToSearch, $person) {
  $attendedEvents = new Pages();
  foreach($eventsPage->children() as $event) {
    if($attended = $event->{$structureField}()->structure()->findBy($fieldToSearch, $person)) {
      $attendedEvents->add($event);
    }
  }
  return $attendedEvents;
}

foreach(page('people')->children() as $person) {
  echo $person->uri();
  $attendedEvents = getAttendedEvents(page('events'), 'in_attendance', 'person', $person->uri());
  foreach($attendedEvents as $event) {
    echo $event->title();
  }
}

?>
1 Like

I have done something similar on a site using $page->update(). I works really well. But there is one (small) problem.

Let’s say I have two pages: Page A & Page B. On both pages I have a field called link that can contain a link to another page. If I edit this field on Page A with a link to Page B and press save. A hook will fire, that updates Page B with a link back to Page A.

So far so good.

The problem is this. Let’s say two editors are editing these two pages at the same time. Editor #1 edits page A and adds a link to Page B, and then presses save. The hook fires and updates Page B with a link back to Page A. But, Editor 2 already has Page B open. When the hook is fired and Page B is updated, Editor 2 has at that point a different version of Page B open, than what is saved to the disc. When Editor 2 later presses save after some edits in other fields, the whole page and all fields will be saved and overwrite the edits that Editor 1 has done.

This problem is a minor one, it should not happen that often. But just the possibility that it can happen, bothers me. It doesn’t feel like a bulletproof solution.

This happens because Kirby updates all fields in the text file upon saving a page. What if this could be changed, so that Kirby can keep track of the fields that have been changed in the pane when a user presses savel, and only updates the changed fields when saving? It should be possible to do. Maybe for Kirby 3?

This might become a problem and that’s why a hook is not always the ideal solution. In the present use case, I’d opt for the second solution I suggested, even if it means that you have to grab the information at runtime. The performance can be optimized by using the cache, though. But you don’t have to store duplicate content and you don’t run into the risk you outlined above.

But this is a general problem, no matter if you use a hook to update, if another user updates the page at the same time or if the page is updated via the frontend. There is already an issue on GitHub.

You can resolve the problem of 2 people having the panel open on the same page, if on one blueprint you don’t declare the list. The panel won’t touch that field and therefore a user can’t undo your hook changes on that field.
Of course that means you can set the a relationship only from one side in the panel while the other is added by the hook (i.e. you can add an event to a person, but you can’t add a person to an event).

However, you should also be aware of another problem; that is when you delete a page.

Let’s say you delete an “event” page: in its delete hook you would want to loop over all the pages in its “In-attendance” field and remove said event from those linked “person” pages.

The problem is that the delete hook fires after the page has already been deleted and you have no more access to the “In-attendance” field. All you get is the page uid and some other stuff, but NO content.
So you would need to loop over ALL the “person” pages and find those that linked to the event.

Kirby is great at modeling one to many relationships (since it has a natural “parent - child” file structure), but it kind of fails with many to many relationships.

I had this problem before and “looping over all pages” for me wasn’t an option since there were about 15k pages I would have to loop over. In my case I was able to retrieve the ids of the related pages from a separate database I used for indexing, but that’s very specific to my case.

Thanks for the advice everyone. Just thought I’d write and close this out.

I ended up just getting the attended events at runtime using tags:

//on mysite.biz/people/person:
$events = $site
		  ->find('events')
		  ->children()
		  ->filterBy( 'event_attendee', $page->title(), ',');

foreach($events as $event):
echo $event->title();
...

And then on the Event page there is a tag field where I’m having people enter event attendees as tags. Works great and there is no apparent overhead.

Thanks again for your help!