Jump to content

Weekly update – 29 July 2022


Recommended Posts

Following up on my July 15th post, I've been working on converting ~1600 reviews from a 3rd party service (BazaarVoice) into ProcessWire for a client's website. 

That conversion is pretty much done now, here's an example page that has a few reviews (click on the Reviews tab). As you can see, we're using the ProcessWire comments field to store not just the review, but also 15 other details and up to 6 photos, per review. This was all done using the new "meta" features of ProcessWire's comments, mentioned in that post linked above. Though I didn't have an example to show you at the time, so wanted to link it here. 

Here's another example of how someone can submit a review here. This form is built in FormBuilder and uses an updated FormBuilderFile Inputfield module which will be bundled in the next version of FormBuilder. This new version of InputfieldFormBuilderFile adds a few useful features such as: 

  • The ability to show a preview image (when uploading photos). 
  • The ability to show just one file input at a time, and reveal additional ones as needed.
  • The ability to include a text or textarea description with each file upload. 

These are all optional and can be enabled or disabled in the field settings. If none of the options are enabled when the output is exactly the same as before, meaning the upgrade should be seamless for existing forms already using the files input. 

The way that a submitted review goes from FormBuilder into a comment is automated with a hook to FormBuilder's "saveForm" method. When the form is submitted, it creates a new pending comment, translating most of the fields into 'meta' data in the comment, there's really not much to it. If you ever have a similar need, be sure to let me know and I'm happy to share the hook code that worked for me. 

In addition to those updates for FormBuilder, this week I've also worked on some related updates to ProcessWire's comments field, especially with improvements to the ProcessCommentsManager module. The module now has a separate dedicated comment editor where you can edit existing comments, and add new comments or replies. Previously you could not add new comments/replies in the comments manager. In the case of the site linked above, they use this particular feature for adding staff replies to reviews. 

I think this comments stuff is largely done now so next week I'll likely be back to working on other parts of the core and modules. Thanks for reading and have a great weekend!


 

  • Like 16
  • Thanks 2
Link to comment
Share on other sites

Thanks for sharing an interesting use of comments. I think I would find the hook code useful as I have FormBuilder and I can think of scenarios where I could use this.

As a matter of interest, if you want to aggregate meta data from all comments and get an average rating for a given piece of meta data, what's the most efficient way to achieve this?

Link to comment
Share on other sites

@Kiwi Chris

Quote

I think I would find the hook code useful as I have FormBuilder and I can think of scenarios where I could use this.

Let's say you've got a FormBuilder form named "review" and it has these fields, all of which are required: 

  • reviewed_page (Page) where user selects what page they are reviewing
  • reviewer_name (Text)
  • reviewer_email (Email)
  • review_text (Textarea)
  • star_rating (Integer)
  • recommend (Toggle Yes/No)

Your comments field is named "comments". We have a template named "form-reviews"  where we output this form using FormBuilder embed method C. Above the embed C output code, in that file we have a hook which takes the submitted review and converts it to a pending comment:

$forms->addHookAfter('FormBuilderProcessor::saveForm', function(HookEvent $event) {

	$processor = $event->object; /** @var FormBuilderProcessor $processor */
	$form = $event->arguments(0); /** @var InputfieldForm $form */
	$field = $event->wire()->fields->get('comments'); /** @var CommentField $field */
	$page = $form->getValueByName('reviewed_page'); /** @var Page $page */
	
	$errors = $processor->getErrors();
	if(count($errors)) return; // if there were errors, do not create a comment yet

	$comment = new Comment();
	$comment->status = Comment::statusPending;
	$comment->cite = $form->getValueByName('reviewer_name');
    $comment->email = $form->getValueByName('reviewer_email');
	$comment->text = $form->getValueByName('review_text');
	$comment->stars = $form->getValueByName('star_rating');

	// example of using meta data
	$comment->setMeta('recommended', $form->getValueByName('recommended')); 

	if($field->addComment($page, $comment, true)) {
		// pending comment added successfully
	} else {
		// failed to add comment
		$processor->addError("Error adding review/comment");
	}
}); 

That's basically it. In my case, I have a lot more "meta" fields, so my hook is longer than this. 

This hook could also be to FormBuilderProcessor::formSubmitSuccess but I choose the saveForm instead because at this point I can still add my own error messages that will be displayed to the user. This gives you the opportunity to do additional validation, should you want to. By the time the formSubmitSuccess method is reached, it's already been determined there were no errors, so at that point you couldn't refuse the form submission if you can't convert it to a Comment for one reason or another. 

Quote

As a matter of interest, if you want to aggregate meta data from all comments and get an average rating for a given piece of meta data, what's the most efficient way to achieve this?

Let's say your Comments field is named "comments", then $page->comments->stars() will return the average of all star ratings on $page (floating point number between 0 and 5.0). For more details, arguments, options see:

https://processwire.com/api/ref/comment-array/stars/
https://processwire.com/api/ref/comment-array/render-stars/
https://processwire.com/api/ref/comment-stars/

For rendering stars it'll use ★ but if you want it to use a different icon you can use CommentStars::setDefault() i.e.

CommentStars::setDefault('star', '<i class="fa fa-star"></i>'));

There are lots of other things you can specify to setDefault() so it's worth looking directly in the file if you decide to use it. 

  • Like 8
Link to comment
Share on other sites

On 7/29/2022 at 10:30 PM, ryan said:

As you can see, we're using the ProcessWire comments field to store not just the review, but also 15 other details and up to 6 photos, per review.

Does that mean we will be able to attach images to comments? I've been looking for such a functionality for a while, that would be great. Could you share a code snippet on how to do it?

Link to comment
Share on other sites

@MrSnoozles 

Quote

Does that mean we will be able to attach images to comments? I've been looking for such a functionality for a while, that would be great. Could you share a code snippet on how to do it?

Yes you can do this, but not directly. There aren't any file/image functions in comments, but you can use $comment->setMeta('name', 'value'); to store whatever you'd like. So in my case, when someone submits a review with photo attachments, I create a directory /site/assets/comments/[page-id]/[comment-id]/ and then copy the files into it. 

To do this, take the saveForm() hook code I posted above and append this into it. Note that 'photos' refers to the name if my files field in FormBuilder, but it's also the name I'm using to store an array of photo files (basenames) in my comment meta data: 

  $entry = $processor->getEntry();
  if($comment->id && !empty($entry['photos']) && $entry['id']) {
    $meta = $comment->getMeta();
    $path = $processor->entries()->getFilesPath($entry['id']);
    $meta['photos'] = addCommentPhotos($comment, $entry, $path); 
    $field->updateComment($page, $comment, [ 'meta' => $meta ]); 
  }

The snippet above got the meta['photos'] from an addCommentPhotos() function. Here's an example of that function below. It copies the photo files out of the entry, validates them, and places them into the /site/assets/ directory I mentioned above. Note that it requires the FileValidatorImage module. 

function addCommentPhotos(Comment $comment, array $entry, $sourcePath) {

  $files = wire()->files;
  $sanitizer = wire()->sanitizer;
  $validator = wire()->modules->get('FileValidatorImage'); /** @var FileValidatorImage $validator */
  $page = $comment->getPage();
  $targetPath = wire()->config->paths->assets . "comments/$page->id/$comment->id/";
  $photos = [];

  // create target directory for files
  if(!is_dir($targetPath)) $files->mkdir($targetPath, true);

  foreach($entry['photos'] as $filename) {
  
    $sourceName = basename($filename); 
    $sourceFile = $sourcePath . $sourceName;

    if(!is_file($sourceFile)) continue; // file not found

    if(!$validator->isValid($sourceFile)) {
      // photo not valid
      $processor->addWarning("Error adding photo $sourceName - " . $validator->getReason()); 
      continue;
    }

    $sourceExt = pathinfo($sourceFile, PATHINFO_EXTENSION);
    $sourceName = pathinfo($sourceFile, PATHINFO_FILENAME);
    $n = 0;

    // ensure filename is unique		
    do {
      $targetName = $sanitizer->fieldName($sourceName);
      if(empty($targetName)) $targetName = 'photo';
      $targetName .= ($n ? "-$n" : "") . ".$sourceExt";
      $targetFile = $targetPath . $targetName;
      $n++;
    } while(is_file($targetFile)); 

    // copy file to target location
    if($files->copy($sourceFile, $targetFile)) {
      // populate file basename in $photos
      $photos[] = $targetName;
    }
  }

  return $photos;
}

That takes care of getting the photos to the right place. When you want to output then with your comments, here's roughly how you might do it: 

foreach($page->comments as $comment) {

  $cite = $comment->getFormatted('cite');
  $text = $comment->getFormatted('text'); 
  $when = wireRelativeTimeStr($comment->created);
  
  echo "<div class='comment'>";
  echo "<p>Posted by: $cite ($when)</p>";
  echo "<p>$text</p>";
  
  $photos = $comment->getMeta('photos');
  
  if(!empty($photos)) {
    $dir = "comments/$page->id/$comment->id/";
    $url = $config->urls->assets . $dir;
    $path = $config->paths->assets . $dir;
    
    echo "<ul class='comment-photos'>";
    
    foreach($photos as $basename) {
      $fileName = $path . $basename;
      $fileUrl = $sanitizer->entities($url . $basename);
      if(file_exists($fileName)) {
        echo "<li><img src='$fileUrl' alt='' /></li>";
      }
    }
    
    echo "</ul>";
  }
  
  echo "</div>";
}

But wait, what about when you do things like delete a page, or an individual comment, and want to make sure the photos are deleted too? You can do this by adding hooks into your /site/ready.php file:

/**
 * After page deleted, delete comment files
 * 
 */
$pages->addHookAfter('deleted', function(HookEvent $event) {
  $page = $event->arguments(0); /** @var Page $page */
  $path = $event->wire()->config->paths->assets . "comments/$page->id/";
  if(is_dir($path)) {
    $event->wire()->files->rmdir($path, true);
    $event->message("Deleted page comments files path: $path"); 
  }
});

/**
 * After individual comment deleted, delete comment files
 *
 */
$wire->addHookAfter('FieldtypeComments::commentDeleted', function(HookEvent $event) {
  $page = $event->arguments(0); /** @var Page $page */
  $comment = $event->arguments(2); /** @var Comment $comment */
  $path = $event->wire()->config->paths->assets . "comments/$page->id/$comment->id/";
  if(is_dir($path)) {
    $event->wire()->files->rmdir($path, true); 
    $event->message("Deleted comment files path: $path"); 
  }
});

 

  • Like 5
Link to comment
Share on other sites

I don't suppose there's any chance of getting these juicy forum board solutions added to related module documentation as Examples, could we? These are great starting points to common scenarios! The downside would be maintaining them for compatibility, but perhaps explicit notices with dates that the examples were created? So often I end up searching the website for answers to questions, but they're all over the place - the blog, documentation, api, forum (and not always the module forum). 😉 Just a thought.

That said, loving these examples you're providing from actual client work you're doing. Thanks, Ryan!

  • Like 4
Link to comment
Share on other sites

You are amazing @ryan. The file previews are gonna be huge for me. The mini app I built with FormBuilder could have really benefitted from it. Not having a preview made it a little ambiguous as to whether the file has been uploaded. I’m getting ready to start another mini app and this will be awesome. Thank you and hoping we can see that in FormBuilder in the next few weeks.

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

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...