Dynamically updating TV value with FormIt

I’m looking to create a simple listing of courses that visitors to my site can book. Each one will have a form that the visitor fills in to book the course, with their name and email address being emailed to me when they book. I’m planning to set up the list of courses using MIGX so I can stipulate course name, description and places available for each course.

When the visitor submits the form to book a course, I’d like the number in the places available field to reduce by 1. Can anyone help me with a snippet that I can use with FormIt to dynamically update the value of the TV when the form submits?

Ideally I’d like all the courses listed on the same page, so the page would include multiple forms (one for each course), but if this adds too much complexity I guess I could have a separate page for each course.

If anyone can help I’d really appreciate it!

Here is some example code that in theory should work as a custom hook for FormIt to change a MIGX-TV.

<?php
$which_course = $hook->formit->config['whichCourse']; //Read property 'whichCourse' of FormIt call

//Read value of TV
$q = array(
    'contentid' => 93, //Resource-ID (the TV is assigned to) <-- CHANGE THIS!!
    'tmplvarid' => 84, //ID of TV <-- CHANGE THIS!!
);
$tvr = $modx->getObject('modTemplateVarResource', $q);
if ($tvr){
    $courses_json = $tvr->get('value');
    $courses = $modx->fromJSON($courses_json); //Convert JSON to array
    
    $changed = false;
    if (is_array($courses)) {
    	//Loop through all the courses
    	foreach($courses as $idx => $course){
    		if ($course['name'] == $which_course){
    			//Decrease the number of places
    			$places = (int) $course['places'];
    			if ($places > 0){
    			    $courses[$idx]['places'] = $places - 1;
    			}
    			$changed = true;
    			break;
    		}
    	}
    }
    
    if ($changed){
    	//The value of MIGX TV has changed
    	$courses_json = $modx->toJSON($courses); //Convert back to JSON
    	$tvr->set('value',$courses_json);
    	$tvr->save();
    }
    return true;
} else {
    $modx->log(modX::LOG_LEVEL_ERROR,'tv not found');
    return false;
}

It assumes that the MIGX-TV has the fields “name” and “places” and that you supply a property &whichCourse in the call to FormIt to define which course to process.

[[!FormIt?
   &hooks=`my_custom_hook`
   &whichCourse=`Course A`
]]

The problem is, that although this snippet changes the value of the TV, this change is not visible in the frontend when you output your data with getImageList (even uncached) until you clear the cache.

So you either have to clear the cache in the hook or write a custom snippet (similar to the snippet above) to output the current number of places available. Or maybe save your data in a custom database table instead of a TV.

1 Like

Thanks @halftrainedharry that’s really helpful. I’ll give that a go and see how I get on. I’ll definitely need the number of places in a TV so that site editors can easily set/amend that value for each course, so I’d like to steer clear of a custom database table, but I guess I could add something along the lines of this to the hook to clear the cache:

$modx->cacheManager->refresh();

I’m a newbie when it comes to writing snippets… would that just go above

return true;

?

Yes, you could call $modx->cacheManager->refresh(); after the line $tvr->save();, but that clears the whole cache of your site every time a user sends a form.

When you use MIGXdb for example, the data is stored in a custom database table but the editors can still change it in a CMP (custom manager page).

1 Like

How do you protect against unauthorised bookings?

My client will be monitoring bookings and requesting extra information from bookers to confirm their place on the course (they need to do this anyway because courses will require medical questionnaires), so will be able to cancel anyone who shouldn’t be on the course. That’s why it’s important that they can easily log into the CMS to change the number of course places available. I’ll add spam protection to the forms to hopefully prevent bots from booking.

maybe, the user should at least confirm their email with kind of a double optin process, before the booking finally is landing into the TV values or in a custom db table (which I would prefer)

I would probably create two custom tables (one for the courses and one for the attendees) and then add a new row to the attendees table (in a FormIt hook) when a user registers.

Here is some example code how you could do something like this with MIGXdb:


In the “Package Manager” create a new package “courses” with this schema.

<?xml version="1.0" encoding="UTF-8"?>
<model package="courses" baseClass="xPDOObject" platform="mysql" defaultEngine="InnoDB" version="1.1">
	<object class="CoursesCourse" table="courses_course" extends="xPDOSimpleObject">
		<field key="name" dbtype="varchar" precision="191" phptype="string" null="false" default=""/>
		<field key="places" dbtype="int" precision="10" attributes="unsigned" phptype="integer" null="false" default="0" />
		
		<composite alias="Attendees" class="CoursesAttendee" local="id" foreign="id_course" cardinality="many" owner="local" />
    </object>
	
    <object class="CoursesAttendee" table="courses_attendee" extends="xPDOSimpleObject">
		<field key="name" dbtype="varchar" precision="191" phptype="string" null="false" default=""/>
		<field key="id_course" dbtype="int" precision="10" attributes="unsigned" phptype="integer" null="false" default="0" />
		
		<aggregate alias="Course" class="CoursesCourse" local="id_course" foreign="id" cardinality="one" owner="foreign" />
		
		<index alias="course" name="course" primary="false" unique="false" type="BTREE">
            <column key="id_course" length="" collation="A" null="false" />
        </index>
    </object>
</model>

Add a new MIGX config “courses” (to be able to edit the courses in the manager)

  • Settings: Name = courses | unique MIGX ID = courses
  • Formtabs:
    • Fieldname = name | Caption = Name
    • Fieldname = places | Caption = Places
    • Caption = Attendees | Input TV type = migxdb | Configs = courses_attendees
  • Columns:
    • Header: ID | Field: id
    • Header: Name | Field: name
  • Contextmenues : update | remove
  • Actionbuttons: addItem
  • MIGXdb-Settings: Package = courses | Classname = CoursesCourse

Add another MIGX config “courses_attendees” (to be able to edit the attendees)

  • Settings: Name = courses_attendees | unique MIGX ID = courses_attendees
  • Formtabs:
    • Fieldname = name | Caption = Name
  • Columns:
    • Header: ID | Field: id
    • Header: Name | Field: name
  • Contextmenues : update | remove
  • Actionbuttons: addItem
  • MIGXdb-Settings: Package = courses | Classname = CoursesAttendee | Load Grid = auto | Check Resource = yes | Join Alias = Course

Add a new menu item: Action = index | Parameters = &configs=courses | Namespace = migx


The hook snippet to add a new row to the attendees table: (Assumes the form has a field “attendee_name”)

<?php
//load package
$base_path = $modx->getOption('core_path').'components/courses/';
$modx->addPackage('courses', $base_path.'model/');

$course_id = $hook->formit->config['courseId']; //Read property 'courseId' of FormIt call

$attendee_name = trim($hook->getValue('attendee_name')); //Read value of form field 'attendee_name'

//create new database entry
$new_t = $modx->newObject('CoursesAttendee');
$new_t->set('name',$attendee_name);
$new_t->set('id_course',$course_id);
$new_t->save();

return true;
[[!FormIt?
   &hooks=`my_custom_hook`
   &courseId=`1`
]]

A snippet to output all the courses:

<?php
//load package
$base_path = $modx->getOption('core_path').'components/courses/';
$modx->addPackage('courses', $base_path.'model/');

$q = $modx->newQuery('CoursesCourse');
$q->select($modx->getSelectColumns('CoursesCourse', 'CoursesCourse'));
$q->select(array('places_taken' => '(SELECT COUNT(id) FROM modx_courses_attendee WHERE id_course = CoursesCourse.id)'));
$courses = $modx->getCollection('CoursesCourse',$q);

$output = '';
foreach ($courses as $course) {
    $course_array = $course->toArray('',false,true);
    $course_array['available_places'] = $course_array['places'] - $course_array['places_taken'];
    $output .= $modx->getChunk('myCourseTpl',$course_array);
}
return $output;

Chunk myCourseTpl:

<h3>[[+name]]</h3>
Total places: [[+places]]<br>
Available places: [[+available_places]]

@halftrainedharry @bruno17 thank you so much for your input.

@halftrainedharry I got your original custom hook working this morning, with the addition of clearing the cache, so this is great to share with my client as a proof of concept for now. It’s still an unpublished page at this stage anyway. When I get a bit of time I’ll look at your latest suggestion of creating the two custom tables – it sounds perfect.

I’ll need to do that once I have a bit more time to get my head around it though! I’ll let you know how I get on.

Thanks again!