Updating a structure file field programmatically using yaml

I am trying to update a files field in a structure programmatically, and have hit an issue. I am generating the yaml from an array like this:

$taskdetails => Data::encode($updatedStructure, "yaml")

Which I am applying using $page->update().

However, in my generated yaml the file field is appearing like this:

filefield: >
    filename.pdf

Which isn’t updating the structure (all the text fields etc. work as expected). When I update the structure through the panel, the field looks like this (which is what I guess I need my yaml to look like):

filefield:
    - >
      filename.pdf

My question is do I need to do something different with the array or the yaml, to get the code to match and the field to update? Thanks for any pointers

The files field content must be yaml encode as well, so encoding the structure alone is not enough to also yaml encode what it contains.

Thanks for the pointer. I have tried using Data::encode on that field, but the output is now:

filefield: |
    - >
    filename.pdf

Closer, but the pipe character shouldn’t be there, and the field is still not updating.

Maybe you can provide more information like your structure field definition and how you are building the array for updating?

Sure,
This is an action triggered by a Uniform form submission. The submitted data then updates a structure on a page. This is the action overall (please see the addded Data:encode for $filename, this is what I have done following your advice above):

class TaskSubmissionAction extends Action
{
    /**
     * Action to process a task submission form, updating the task for the school
     */
    public function perform() {

        $taskautoid = $this->form->data('taskautoid');
        $taskdetails = $this->form->data('taskdetails');
        $information = $this->form->data('information');
        $link = $this->form->data('link');
        $filefield = $this->form->data('filefield');
        $prefix = $this->form->data('prefix');
        if(!empty($filefield['name'])){
            $filename = $prefix . $filefield['name'];
        }
        $structure = kirby()->user()->school()->toPage()->$taskdetails()->yaml();

        try {
            foreach ($structure as $item){
                // this is school's item
                if ($item['questionautoid'] == $taskautoid){
                  $item['progress'] = 'pending';
                  $item['information'] = $information;
                  $item['link'] = $link;
                  if(!empty($filefield['name'])){
                    $item['filefield'] = Data::encode($filename, "yaml");
                  }
                }
                
                $updatedStructure[] = $item;
              }
      
              kirby()->user()->school()->toPage()->update([
                $taskdetails => Data::encode($updatedStructure, "yaml"),
              ]);
        } catch (\Exception $e) {
            $this->fail($e->getMessage());
        }
    }
}

The other fields (link, information) are updating as expected. Structure field definition looks like this:

fields:
              questionAutoid:
                type: hidden
                translate: false
              nextTask:
                label: Suggest as Next
                type: toggle
                width: 1/4
                text: 
                  - "No"
                  - "Yes" 
              task:
                label: Task
                type: text
                disabled: true
              details:
                type: text
                disabled: true
                label: Additional details of task requirements
              textBox:
                type: hidden
              fileUpload:
                type: hidden
              linkBox:
                type: hidden
              timeToComplete:
                type: hidden
              textBoxRequired:
                type: hidden
              fileUploadRequired:
                type: hidden
              linkBoxRequired:
                type: hidden
              information:
                width: 1/3
                type: textarea
                buttons: false
                label: Submitted information (if applicable)
              link:
                width: 1/3
                type: text
                label: Submitted link (if applicable)
              fileField:
                width: 1/3
                type: files
                multiple: false
                label: Submitted file (if applicable)
                uploads: false
              progress:
                type: select
                default: incomplete
                options:
                  incomplete: 🔴 Incomplete
                  pending: 🟠 Pending
                  complete: 🟢 Approved

I am totally lost with this :sweat_smile:. This is what my array looks like:

[questionautoid] => h78df0bn
[nexttask] => false
[task] => Appoint a member of the SLT as leader of the project
[details] => Upload a letter from your Headteacher with the SLT designated lead on the project
[textbox] => false
[fileupload] => true
[linkbox] => false
[timetocomplete] => 1 hour
[textboxrequired] => false
[fileuploadrequired] => true
[linkboxrequired] => false
[information] => 
[link] => 
[filefield] => filename.pdf
[progress] => pending

And when I convert to yaml to use with $page->update() I get this:

questionautoid: h78df0bn
nexttask: 'false'
 task: >
    Appoint a member of the SLT as leader of
    the project
 details: >
    Upload a letter from your Headteacher
    with the SLT designated lead on the
    project
 textbox: 'false'
 fileupload: 'true'
 linkbox: 'false'
 timetocomplete: 1 hour
 textboxrequired: 'false'
 fileuploadrequired: 'true'
 linkboxrequired: 'false'
 information: ""
 link: ""
 filefield: >
    filename.pdf
 progress: pending

All the fields apart from the filefield update successfully. When I save the structure through the panel I get the following, with the different treatment of the filefield:

questionautoid: h78df0bn
nexttask: 'false'
task: >
  Appoint a member of the SLT as leader of
  the project
details: >
  Upload a letter from your Headteacher
  with the SLT designated lead on the
  project
textbox: 'false'
fileupload: 'true'
linkbox: 'false'
timetocomplete: 1 hour
textboxrequired: 'false'
fileuploadrequired: 'true'
linkboxrequired: 'false'
information: ""
link: ""
filefield:
  - >
    filename.pdf
progress: pending

If anyone has any thoughts it would be greatly appreciated, I am not sure what to try next

I don’t know if it makes any difference, but what if you wrap the filename in an array:

Data::encode([$filename], 'yaml')

No, unfortunately that didn’t work either. I am stumped!

I think this may actually be a bug. This code works to update the same structure setup in a user:

class TaskSubmissionAction extends Action
{
    public function perform() {

        $taskautoid = $this->form->data('taskautoid');
        $taskdetails = $this->form->data('taskdetails');
        $information = $this->form->data('information');
        $link = $this->form->data('link');
        $filefield = $this->form->data('filefield');
        $prefix = $this->form->data('prefix');
        if(!empty($filefield['name'])){
            $filename = $prefix . $filefield['name'];
        }
        $structure = kirby()->user()->$taskdetails()->yaml();

        try {
            foreach ($structure as $item){
                // this is user's item
                if ($item['questionautoid'] == $taskautoid){
                  $item['progress'] = 'pending';
                  $item['information'] = $information;
                  $item['link'] = $link;
                  if(!empty($filefield['name'])){
                    $item['filefield'] = $filename;
                  }
                }
                
                $updatedStructure[] = $item;
              }
      
              kirby()->user()->update([
                $taskdetails => Data::encode($updatedStructure, "yaml"),
              ]);
        } catch (\Exception $e) {
            $this->fail($e->getMessage());
        }
    }
}

And I am handling updating the file in the same way as above. If anyone has a chance to take a look over the weekend that would be awesome, I am not sure where to go next

That is indeed weird. I’ll try to check this out over the weekend. Which Kirby version are u using and where are the files actually uploaded to?

Thanks I really appreciate it! The files are uploaded to the “school” page the structure is on - that is handled by the standard Uniform uploadAction.

I am on Kirby 3.5.6, but can upgrade if that will help.

The broader picture is I am refactoring the site to manage this data on a school page, rather than a user account, hence having the working code for a user.

Friendly reminder of Yaml::encode($yourVariable), which does the same as

Data::encode($yourVariable, 'yaml ')

… but that’s not helping, I know :slight_smile:

Yes, that doesn’t make any difference, although Yaml::encode is the old Kirby 2 way of doing it and might get deprecated some day?

@mikeharrison

I set up a very simple test page for testing with just a structure field. I then created a page with this template and put a file into the folder.

title: Test

fields:
  details:
    type: structure
    fields:
      info:
        type: text
      filesfield:
        type: files
        multiple: false
        uploads: false

Then in a template, I updated the page like this:

$p = page('test');
$data = $p->details()->yaml();
$newItem = [
  'info' => 'Some text',
  'filesfield' => Data::encode(['himalaya.jpg'], 'yaml'),
];

$data[] = $newItem;

$data = Data::encode($data, 'yaml');

$kirby->impersonate('kirby');

try {
  $p->update([
    'details' => $data,
  ]);
} catch(Exception $e) {
  return $e->getMessage();
}

This works as expected and stores the following data:

Details:

- 
  info: Some text
  filesfield:
    - himalaya.jpg

I then repeated this with a non-existing filename, result:

Details:

- 
  info: Some text
  filesfield:
    - himalaya.jpg
- 
  info: Some text
  filesfield: [ ]

Tested with Kirby 3.5.7.1

So if this simple testcase doesn’t work in your environment, then maybe it is indeed a bug in the old version and updating might help.

One thing that you should keep in mind is that since you are modifying your variable instead of adding to it, you should prepend an ampersand before your variable in the loop:

foreach ($structure as &$item) {

Thank you @texnixe I really appreciate you taking a look. I will give that code a try as soon as I get chance.

Hi,

I have added your code into a test action in Uniform, and created a new ‘test’ page and blueprint/template. This is the code I have as my test action:

class TestAction extends Action
{
    public function perform() {
        $p = page('test');
        $data = $p->details()->yaml();
        $newItem = [
        'info' => 'Some text',
        'filesfield' => Data::encode(['himalaya.jpg'], 'yaml'),
        ];

        $data[] = $newItem;

        $data = Data::encode($data, 'yaml');

        try {
        $p->update([
            'details' => $data,
        ]);
        } catch(Exception $e) {
        return $e->getMessage();
        }
    }
}

I have this run when I submit my form, so the trigger is the same. On submit I get the following saved:

Details:

- 
  info: Some text
  filesfield: [ ]

I have also updated to the latest version of Kirby to see if that helped, but am getting the same result. I am not sure what to try with this next, and I am not getting any errors logged anywhere to go from

You get the empty array when the file doesn’t exist in the page folder.

Ok that is interesting. If I add the file manually, then put the filename into the action, it works. Perhaps it is to do with how the Uniform actions work - I assumed they were sequential (I have the upload action before this one), but perhaps the file is not being fully written until after the action has run, hence the empty array. @mzur could that be the case?

No, actions are executed immediately when they are called in the code. Did you take the random filename prefix into account that can be added by the upload action? How does the actual code look that calls Uniform?

Thanks for the quick reply.

This is the complete page controller code:

<?php

use Uniform\Form;

return function ($kirby, $page)
{

    $forms = [];
    $formDefinitions = $page->keyTasks()->toStructure()->toArray();

    foreach($formDefinitions as $instance) {
        $instanceKey = $instance['autoid'];
        $forms[$instanceKey] = new Form([
            'filefield' => [
                'rules' => [
                    'file',
                    'mime' => ['application/pdf','application/msword','application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
                    'filesize' => 5000,
                ],
                'message' => [
                    'Please choose a file.',
                    'Please choose a PDF or Word document.',
                    'Please choose a file that is smaller than 5 MB.',
                ],
            ],
            'information' => [],
            'link' => [
                'rules' => ['url'],
                'message' => 'Please enter a valid url',
            ],
            'taskdetails' => [],
            'taskautoid' => [],
            'task' => [],
            'prefix' => [],
        ], $instanceKey);
    }

    if ($kirby->request()->is('POST')) {
        $contactEmail = kirby()->user()->school()->toPage()->mentorEmail()->isNotEmpty()
            ? strval(kirby()->user()->school()->toPage()->mentorEmail())
            : strval(kirby()->site()->contactEmail());
        $userEmail = strval(kirby()->user()->email());

        $forms[$instanceKey]
            ->withoutGuards()
            ->uploadAction(['fields' => [
                'filefield' => [
                    'target' => kirby()->user()->school()->toPage()->contentFileDirectory(),
                    'prefix' => get('prefix'),
                ],
            ]])
            ->emailAction([
                // standard email bits
            ])
            ->emailAction([
                // standard email bits
            ])
            ->taskSubmissionAction(); // this is the custom action


    }

    return compact('forms');
};

?>

And for completeness, this is what I have now for the taskSubmissionAction:

class TaskSubmissionAction extends Action
{
    /**
     * Action to process a task submission form, updating the task for the school
     */
    public function perform() {

        $taskautoid = $this->form->data('taskautoid');
        $taskdetails = $this->form->data('taskdetails');
        $information = $this->form->data('information');
        $link = $this->form->data('link');
        $filefield = $this->form->data('filefield');
        $prefix = $this->form->data('prefix');
        if(!empty($filefield['name'])){
            $filename = $prefix . $filefield['name'];
        }
        $structure = kirby()->user()->school()->toPage()->$taskdetails()->yaml();
        // Now we have the structure as an array...

        try {
            foreach ($structure as &$item){
                // this is school's item
                if ($item['questionautoid'] == $taskautoid){
                  $item['progress'] = 'pending';
                  $item['information'] = $information;
                  $item['link'] = $link;
                  if(!empty($filefield['name'])){
                    $item['filefield'] = Data::encode([$filename], 'yaml');
                  }
                }
                
                $updatedStructure[] = $item;
              }

              $encodedUpdatedStructure = Data::encode($updatedStructure, "yaml");

              kirby()->user()->school()->toPage()->update([
                $taskdetails => $encodedUpdatedStructure,
              ]);

        } catch (\Exception $e) {
            $this->fail($e->getMessage());
        }
    }
}