Jump to content

Hooks: rename all files on a page after save()


snck
 Share

Recommended Posts

I am using FormBuilder to collect data and save it to pages. The forms (and the page templates used) use file fields to collect PDF files. The uploaded files should automatically be renamed to match the name of the fields. This should happen every time a page is saved. The first time after the form is saved to the page, but also on every other page save, because editors could have added or exchanged some of the files and then they should be renamed again.

I tried to solve this in different ways, but nothing really worked as expected. What I tried:

$this->wire->addHookBefore("Pages::saveReady(template^=application_entry)", function(HookEvent $event) {

  $page = $event->arguments(0);

  // get all fields of fieldtype file
  $fileFields = $page->fields->find('type=FieldtypeFile');

  // loop through all file fields
  foreach($fileFields as $field){

    $fieldname = $field->name;
    $field = $page->get($field->name);
    $index = 1;

    foreach($field as $item){

      $newFilename = $fieldname;
      // if there are multiple files, add index to filename
      if(count($field) > 1){
        $newFilename .= "-".$index;
        $index++;
      } 
      $newFilename .= '.'.$item->ext;
      // bd($item->basename()." -> ".$newFilename);
      // only rename if filename is not already set
      if($item->basename() != $newFilename){
        $item->rename($newFilename);
      }
    }
  }
  $event->arguments(0, $page);
});

When I save an existing page, the files are renamed, but I get the following error message and the filename change is not reflected in the database:

Quote

WireFileTools: rename: Given pathname ($oldName) that does not exist: /var/www/html/site/assets/files/-8176/original_filename.pdf

If the page has not existed before save (got created by FormBuilder) I get a message that it was created, but it does not get created and FormBuilder shows a Not yet created in the entries list.

Another approach was this: 

$this->wire->addHookAfter("Pages::saved(template^=application_entry)", function(HookEvent $event) {

  $page = $event->arguments(0);

  // get all fields of fieldtype file
  $fileFields = $page->fields->find('type=FieldtypeFile');

  $page->of(false);

  // loop through all file fields
  foreach($fileFields as $field){

    $fieldname = $field->name;
    $field = $page->get($field->name);
    $index = 1;

    foreach($field as $item){

      $newFilename = $fieldname;
      // if there are multiple files, add index to filename
      if(count($field) > 1){
        $newFilename .= "-".$index;
        $index++;
      } 
      $newFilename .= '.'.$item->ext;

      bd($item->basename()." -> ".$newFilename);
      // only rename if filename is not already set
      if($item->basename() != $newFilename){
        $item->rename($newFilename);
        $page->save();
      }
    }
  }
});

This also renames the file on save, but the filename in the field is still the old name. I am using pagefileSecure on this template (if that matters).

Any help his highly appreciated!

Thanks in advance,
Flo

Link to comment
Share on other sites

@ryan  Today I made another observation. I used the rename() method on the Pagefiles object and got a fatal error:

$this->wire->addHookAfter("Pages::saved(template^=application_entry)", function(HookEvent $event) {

  $page = $event->arguments(0);

  // get all fields of fieldtype file
  $fileFields = $page->fields->find('type=FieldtypeFile');

  $page->of(false);

  // loop through all file fields
  foreach($fileFields as $field){

    $fieldname = $field->name;
    $field = $page->get($field->name);
    $index = 1;

    foreach($field as $item){

      $newFilename = $fieldname;
      // if there are multiple files, add index to filename
      if(count($field) > 1){
        $newFilename .= "-".$index;
        $index++;
      } 
      $newFilename .= '.'.$item->ext;

      bd($item->basename()." -> ".$newFilename);
      // only rename if filename is not already set
      if($item->basename() != $newFilename){
        // $item->rename($newFilename); 
        $field->rename($item, $newFilename);
      }
    }
  }
  $page->save();
});

1395427881_Bildschirmfoto2023-12-19um23_35_56.thumb.png.eed3df7b5ba1da08d7cd7931e7fff8af.png

1024 MiB exhausted?

Link to comment
Share on other sites

Just from skimming through the code, I can see that you are creating an exponential endless loop. You are hooking the saved event for a page and then - for every file field - saving said page again inside the hook. This will trigger another saved hook for every file field which will loop endlessly.

I think this is why you are getting the out-of-memory error from your second post. I would try the saveReady hook to accomplish this. Don't call page->save() in there because you factually change the stuff before the page is ever saved (your changes will be saved automatically).

  • Like 1
Link to comment
Share on other sites

@poljpocket Thanks! Wanted to delete that (stupid) post, but since you referenced it, I'll leave it where it is. 😉

My problem with saveReady is that this only works for pages (and files) that have been saved before (so files are already present). I think I got a lot closer to what I want to achieve with this:

$this->wire->addHookAfter("Pages::save(template^=application_entry)", function(HookEvent $event) {

  $page = $event->arguments(0);

  if($page->savedByThisHook) return;

  bd("save hook triggered");

  // get all fields of fieldtype file
  $fileFields = $page->fields->find('type=FieldtypeFile');
  // bd($fileFields);
    
  $page->of(false);
  $empty_pagefiles = false;
  $renamedFiles = false;

  // loop through all file fields
  foreach($fileFields as $fileField){

    $fieldname = $fileField->name;
    $pagefiles = $page->get($fieldname);
    $index = 1;

    // bd($pagefiles);

    if($pagefiles->count == 0){
      $empty_pagefiles = true;
    } else {
      foreach($pagefiles as $pagefile){
        $newFilename = $fieldname;
        // if there are multiple files, add index to filename
        if(count($pagefiles) > 1){
          $newFilename .= "-".$index;
          $index++;
        } 
        $newFilename .= '.'.$pagefile->ext;
  
        // only rename if filename is not already set
        if($pagefile->basename() != $newFilename){
          $page->get($fieldname)->trackChange('basename');
          bd("renamed: ".$pagefile->basename()." -> ".$newFilename);
          $pagefile->rename($newFilename);
          $pagefile->save();
          $renamedFiles = true;
        } else {
          bd("not renamed: ".$pagefile->basename()." -> ".$newFilename);
        }
      }
    }
  }

  if($empty_pagefiles === true){
    // $pagefiles still empty, not yet installed?
    // add hook!
    $event->addHookAfter('Pagefile::install', function(HookEvent $event) use($page) {
      // trigger page save again to rename files
      bd("conditional hook triggered, installed file". $event->arguments(0));
      bd("is_file: ".is_file($event->arguments(0)));
      $page->savedByThisHook = false;
      $page->save();
      $event->removeHook(null);
    });
  }

  if($renamedFiles === true){
    $page->savedByThisHook = true; // prevent infinite loop
    $page->save();
  }
});

The key to finally having the database reflect the filename changes was 

$page->get($fieldname)->trackChange('basename');

This works fine for pages that have been saved before, but I can only rename files that are already present in the filesystem, which seems to happen while/after save() has been called. Is there any method I did not find (maybe it's too late) that I can use elegantly that fires when alle pagefiles are already present?

My strategy was to add another conditional hook when I recognize a $pagefiles->count of 0 after Pagefile::install, save the page again (and in consequence trigger the renaming process). This works almost the way it should, because it triggers the renaming for 2 of 3 files, but the 3rd will never be triggered automatically (without saving the page manually again). This is what happens:

878684391_Bildschirmfoto2023-12-20um02_50_19.thumb.png.90d7fd5a8c5b1d86934f42d266ca0ee0.png

Although this does not look as elegant (and short) as I thought it could, it does almost what I want. As I feel very close, I'd be really thankful for help. Maybe there is a completely different approach to this problem? Or maybe just small changes that make it work the way I want?

Link to comment
Share on other sites

1 hour ago, snck said:

which seems to happen while/after save() has been called. Is there any method I did not find (maybe it's too late) that I can use elegantly that fires when alle pagefiles are already present?

You can hook "saved" method.

When you don't want to trigger hooks on a page save you can use:

$page->save(options: ['noHooks' => true]);

 

  • Like 2
Link to comment
Share on other sites

@da² Thanks for your input!

Regarding save vs. saved:

Quote

This is the same as hooking after Pages::save, except that it occurs before other save-related hooks. Whereas Pages::save hooks occur after. In most cases, the distinction does not matter. (https://processwire.com/api/ref/pages/saved/)

This is why I chose save, but both should work.

I was aware of the 'noHooks' option, but thinking it would prevent the page updating after the rename action. 

I finally found a solution that serves all my needs and works well so far. I outsourced the renaming part to a function:

function renameApplicationEntryFiles($page){
  if($page->parent->template->name == "application_entries"){
    // only do this for application entries
    $fileFields = $page->fields->find('type=FieldtypeFile');
    $page->of(false);
    $renamedFiles = false;
    foreach($fileFields as $fileField){
      $fieldname = $fileField->name;
      $pagefiles = $page->get($fieldname);
      $index = 1;
      foreach($pagefiles as $pagefile){
        $newFilename = $fieldname;
        // if there are multiple files, add index to filename
        if(count($pagefiles) > 1){
          $newFilename .= "-".$index;
          $index++;
        } 
        $newFilename .= '.'.$pagefile->ext;
        // only rename if filename is not already set
        if($pagefile->basename() != $newFilename){
          $page->get($fieldname)->trackChange('basename');
          bd("renamed: ".$pagefile->basename()." -> ".$newFilename);
          $pagefile->rename($newFilename);
          $pagefile->save();
          $renamedFiles = true;
        } 
      }
    }
    if($renamedFiles === true){
      $page->save(['noHooks' => true]);
      return true;
    }
  } else {
    return false;
  }
}

This function gets called on save and through the conditional Pagefile::install hook which covers all of the cases I need to cover:

$this->wire->addHookAfter("Pages::save(template^=application_entry)", function(HookEvent $event) {
  $page = $event->arguments(0);
  $fileFields = $page->fields->find('type=FieldtypeFile');
  $empty_pagefiles = false;
  foreach($fileFields as $fileField){
    $fieldname = $fileField->name;
    $pagefiles = $page->get($fieldname);
    if($pagefiles->count == 0){
      $empty_pagefiles = true;
    } 
  }
  if($empty_pagefiles === true){
    $event->addHookAfter('Pagefile::install', function(HookEvent $event) use($page) {
      renameApplicationEntryFiles($page);
      $event->removeHook(null);
    });
  } else {
    renameApplicationEntryFiles($page);
  }
});

Maybe this journey can be useful for someone, I had a hard time finding forum threads dealing with hooks related to Pagefiles. And of course I am still open for suggestions on how to optimise my solution. 🙂

Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

×
×
  • Create New...