Skip to content

Instantly share code, notes, and snippets.

@123andy
Created April 17, 2020 17:18
Show Gist options
  • Save 123andy/e869115f69a6dc6a04651b40e0bc41c5 to your computer and use it in GitHub Desktop.
Save 123andy/e869115f69a6dc6a04651b40e0bc41c5 to your computer and use it in GitHub Desktop.
A helper object for working with repeating forms in REDCap - this is a WIP...
<?php
class RepeatingForms
{
// Metadata
private $Proj;
private $pid;
private $is_longitudinal;
private $data_dictionary;
private $fields;
private $events_enabled = array(); // Array of event_ids where the instrument is enabled
private $instrument;
public $is_survey;
private $survey_id;
// Instance
private $event_id;
private $data;
private $data_loaded = false;
private $record_id;
// Last error message
public $last_error_message = null;
function __construct($pid, $instrument_name)
{
global $Proj, $module;
if ($Proj->project_id == $pid) {
$this->Proj = $Proj;
} else {
$this->Proj = new \Project($pid);
}
if (empty($this->Proj) or ($this->Proj->project_id != $pid)) {
$this->last_error_message = "Cannot determine project ID in RepeatingForms";
return false;
}
$this->pid = $pid;
// Get the instrument
$this->instrument = $instrument_name;
if (!isset($this->Proj->forms[$this->instrument])) {
$this->last_error_message = "Form $this->instrument is not valid in this project";
return false;
}
// Find the fields on this repeating instrument
$this->data_dictionary = \REDCap::getDataDictionary($pid, 'array', false, null, array($instrument_name));
$this->fields = array_keys($this->data_dictionary);
// Is this project longitudinal?
$this->is_longitudinal = $this->Proj->longitudinal;
// If this is not longitudinal, retrieve the event_id
if (!$this->is_longitudinal) {
$this->event_id = array_keys($this->Proj->eventInfo)[0];
}
// Is this instrument a survey
$this->is_survey = isset($this->Proj->forms[$instrument_name]["survey_id"]);
if ($this->is_survey) $this->survey_id = $this->Proj->forms[$instrument_name]["survey_id"];
// Retrieved events
$all_events = $this->Proj->getRepeatingFormsEvents();
// Make sure form is repeating
$is_repeating = false;
foreach ($this->Proj->RepeatingFormsEvents as $event_id => $forms) {
if (isset($forms[$this->instrument])) {
$is_repeating = true;
break;
}
}
if (!$is_repeating) $this->last_error_message = "$this->instrument is not set as a repeating form in any events";
// See which events have this form enabled
foreach (array_keys($all_events) as $event) {
$fields_in_event = \REDCap::getValidFieldsByEvents($this->pid, $event, false);
$field_intersect = array_intersect($fields_in_event, $this->fields);
if (isset($field_intersect) && sizeof($field_intersect) > 0) {
array_push($this->events_enabled, $event);
}
}
}
/**
* This function will load data internally from the database using the record, event and optional
* filter in the calling arguments here as well as pid and instrument name from the constructor. The data
* is saved internally in $this->data. The calling program must then call one of the get* functions
* to retrieve the data.
*
* @param $record_id
* @param null $event_id
* @param null $filter
* @return None
*/
public function loadData($record_id, $event_id=null, $filter=null)
{
global $module;
$this->record_id = $record_id;
if (!is_null($event_id)) {
$this->event_id = $event_id;
}
// Filter logic will only return matching instances
$return_format = 'array';
$repeating_forms = \REDCap::getData($this->pid, $return_format, array($record_id), $this->fields, $this->event_id, NULL, false, false, false, $filter, true);
// If this is a classical project, we are not adding event_id.
foreach (array_keys($repeating_forms) as $record) {
foreach ($this->events_enabled as $event) {
if (!is_null($repeating_forms[$record]["repeat_instances"][$event]) and !empty($repeating_forms[$record_id]["repeat_instances"][$event])) {
if ($this->is_longitudinal) {
$this->data[$record_id][$event] = $repeating_forms[$record_id]["repeat_instances"][$event][$this->instrument];
} else {
$this->data[$record_id] = $repeating_forms[$record_id]["repeat_instances"][$event][$this->instrument];
}
}
}
}
$this->data_loaded = true;
}
/**
* This function will return the data retrieved based on a previous loadData call. All instances of an
* instrument fitting the criteria specified in loadData will be returned. See the file header for the
* returned data format.
*
* @param $record_id
* @param null $event_id
* @return array (of data loaded from loadData) or false if an error occurred
*/
public function getAllInstances($record_id, $event_id=null) {
if ($this->is_longitudinal && is_null($event_id)) {
$this->last_error_message = "You must supply an event_id for longitudinal projects in " . __FUNCTION__;
return false;
} else if (!$this->is_longitudinal) {
$event_id = $this->event_id;
}
// Check to see if we have the correct data loaded. If not, load it.
if ($this->data_loaded == false || $this->record_id != $record_id || $this->event_id != $event_id) {
$this->loadData($record_id, $event_id, null);
}
return $this->data;
}
/**
* This function will return one instance of data retrieved in dataLoad using the $instance_id.
*
* @param $record_id
* @param $instance_id
* @param null $event_id
* @return array (of instance data) or false if an error occurs
*/
public function getInstanceById($record_id, $instance_id, $event_id=null)
{
if ($this->is_longitudinal && is_null($event_id)) {
$this->last_error_message = "You must supply an event_id for longitudinal projects in " . __FUNCTION__;
return false;
} else if (!$this->is_longitudinal) {
$event_id = $this->event_id;
}
// Check to see if we have the correct data loaded.
if ($this->data_loaded == false || $this->record_id != $record_id || $this->event_id != $event_id) {
$this->loadData($record_id, $event_id, null);
}
// If the record and optionally event match, return the data.
if ($this->is_longitudinal) {
if (!empty($this->data[$record_id][$event_id][$instance_id]) &&
!is_null($this->data[$record_id][$event_id][$instance_id])) {
return $this->data[$record_id][$event_id][$instance_id];
} else {
$this->last_error_message = "Instance number is invalid";
return false;
}
} else {
if (!empty($this->data[$record_id][$instance_id]) && !is_null($this->data[$record_id][$instance_id])) {
return $this->data[$record_id][$instance_id];
} else {
$this->last_error_message = "Instance number is invalid";
return false;
}
}
}
/**
* This function will return the first instance_id for this record and optionally event. This function
* does not return data. If the instance data is desired, call getInstanceById using the returned instance id.
*
* @param $record_id
* @param null $event_id
* @return int (instance number) or false (if an error occurs)
*/
public function getFirstInstanceId($record_id, $event_id=null) {
if ($this->is_longitudinal && is_null($event_id)) {
$this->last_error_message = "You must supply an event_id for longitudinal projects in " . __FUNCTION__;
return false;
} else if (!$this->is_longitudinal) {
$event_id = $this->event_id;
}
// Check to see if we have the correct data loaded.
if ($this->data_loaded == false || $this->record_id != $record_id || $this->event_id != $event_id) {
$this->loadData($record_id, $event_id, null);
}
// If the record and optionally event match, return the data.
if ($this->is_longitudinal) {
if (!empty(array_keys($this->data[$record_id][$event_id])[0]) &&
!is_null(array_keys($this->data[$record_id][$event_id])[0])) {
return array_keys($this->data[$record_id][$event_id])[0];
} else {
$this->last_error_message = "There are no instances in event $this->event_id for record $record_id " . __FUNCTION__;
return false;
}
} else {
if (!empty(array_keys($this->data[$record_id])[0]) && !is_null(array_keys($this->data[$record_id])[0])) {
return array_keys($this->data[$record_id])[0];
} else {
$this->last_error_message = "There are no instances for record $record_id " . __FUNCTION__;
return false;
}
}
}
/**
* This function will return the last instance_id for this record and optionally event. This function
* does not return data. To retrieve data, call getInstanceById using the returned $instance_id.
*
* @param $record_id
* @param null $event_id
* @return int | false (If an error occurs)
*/
public function getLastInstanceId($record_id, $event_id=null) {
if ($this->is_longitudinal && is_null($event_id)) {
$this->last_error_message = "You must supply an event_id for longitudinal projects in " . __FUNCTION__;
return false;
} else if (!$this->is_longitudinal) {
$event_id = $this->event_id;
}
// Check to see if we have the correct data loaded.
if ($this->data_loaded == false || $this->record_id != $record_id || $this->event_id != $event_id) {
$this->loadData($record_id, $event_id, null);
}
// If the record_ids (and optionally event_ids) match, return the data.
if ($this->is_longitudinal) {
$size = sizeof($this->data[$record_id][$event_id]);
if ($size < 1) {
$this->last_error_message = "There are no instances in event $event_id for record $record_id " . __FUNCTION__;
return false;
} else {
return array_keys($this->data[$record_id][$event_id])[$size - 1];
}
} else {
$size = sizeof($this->data[$record_id]);
if ($size < 1) {
$this->last_error_message = "There are no instances for record $record_id " . __FUNCTION__;
return false;
} else {
return array_keys($this->data[$record_id])[$size - 1];
}
}
}
/**
* This function will return the next instance_id in the sequence that does not currently exist.
* If there are no current instances, it will return 1.
*
* @param $record_id
* @param null $event_id
* @return int | false (if an error occurs)
*/
public function getNextInstanceId($record_id, $event_id=null)
{
// If this is a longitudinal project, the event_id must be supplied.
if ($this->is_longitudinal && is_null($event_id)) {
$this->last_error_message = "You must supply an event_id for longitudinal projects in " . __FUNCTION__;
return false;
} else if (!$this->is_longitudinal) {
$event_id = $this->event_id;
}
// Find the last instance and add 1 to it. If there are no current instances, return 1.
$last_index = $this->getLastInstanceId($record_id, $event_id);
if (empty($last_index)) {
return 1;
} else {
return ++$last_index;
}
}
/**
* This function will save an instance of data. If the instance_id is supplied, it will overwrite
* the current data for that instance with the supplied data. An instance_id must be supplied since
* instance 1 is actually stored as null in the database. If an instance is not supplied, an error
* will be returned.
*
* @param $record_id
* @param $data
* @param null $instance_id
* @param null $event_id
* @return true | false (if an error occurs)
*/
public function saveInstance($record_id, $data, $instance_id = null, $event_id = null)
{
if ($this->is_longitudinal && is_null($event_id)) {
$this->last_error = "Event ID Required for longitudinal project in " . __FUNCTION__;
return false;
} else if (!$this->is_longitudinal) {
$event_id = $this->event_id;
}
// If the instance ID is null, get the next one because we are saving a new instance
if (is_null($instance_id)) {
$this->last_error = "Instance ID is required to save data " . __FUNCTION__;
return false;
} else {
$next_instance_id = $instance_id;
}
// Include instance and format into REDCap expected format
$new_instance[$record_id]['repeat_instances'][$event_id][$this->instrument][$next_instance_id] = $data;
$return = REDCap::saveData($this->pid, 'array', $new_instance);
if (!isset($return["errors"]) and ($return["item_count"] <= 0)) {
$this->last_error = "Problem saving instance $next_instance_id for record $record_id in project $this->pid. Returned: " . json_encode($return);
return false;
} else {
return true;
}
}
// TBD: Not sure how to delete an instance ????
public function deleteInstance($record_id, $instance_id, $event_id = null) {
global $module;
$module->emLog("This is the pid in deleteInstance $this->pid for record $record_id and instance $instance_id and event $event_id");
// If longitudinal and event_id = null, send back an error
if ($this->is_longitudinal && is_null($event_id)) {
$this->last_error = "Event ID Required for longitudinal project in " . __FUNCTION__;
return false;
} else if (!$this->is_longitudinal) {
$event_id = $this->event_id;
}
$this->last_error = "Delete instance is not implemented yet!" . __FUNCTION__;
return false;
}
/**
* Return the data dictionary for this form
*
* @return array
*/
public function getDataDictionary()
{
return $this->data_dictionary;
}
/**
* This function will look for the data supplied in the given record/event and send back the instance
* number if found. The data supplied does not need to be all the data in the instance, just the data that
* you want to search on.
*
* @param $needle
* @param $record_id
* @param null $event_id
* @return int | false (if an error occurs)
*/
public function exists($needle, $record_id, $event_id=null) {
// Longitudinal projects need to supply an event_id
if ($this->is_longitudinal && is_null($event_id)) {
$this->last_error = "Event ID Required for longitudinal project in " . __FUNCTION__;
return false;
} else if (!$this->is_longitudinal) {
$event_id = $this->event_id;
}
// Check to see if we have the correct data loaded.
if ($this->data_loaded == false || $this->record_id != $record_id || $this->event_id != $event_id) {
$this->loadData($record_id, $event_id, null);
}
// Look for the supplied data in an already created instance
$found_instance_id = null;
$size_of_needle = sizeof($needle);
if ($this->is_longitudinal) {
foreach ($this->data[$record_id][$event_id] as $instance_id => $instance) {
$intersected_fields = array_intersect_assoc($instance, $needle);
if (sizeof($intersected_fields) == $size_of_needle) {
$found_instance_id = $instance_id;
}
}
} else {
foreach ($this->data[$this->record_id] as $instance_id => $instance) {
$intersected_fields = array_intersect_assoc($instance, $needle);
if (sizeof($intersected_fields) == $size_of_needle) {
$found_instance_id = $instance_id;
}
}
}
// Supplied data did not match any instance data
if (is_null($found_instance_id)) {
$this->last_error_message = "Instance was not found with the supplied data " . __FUNCTION__;
}
return $found_instance_id;
}
/**
* Obtain the survey url for this instance of the form
* @param $record
* @param $instance_id
* @return string|null
*/
public function getSurveyUrl($record, $instance_id) {
// Make sure the instrument is a survey
if(!$this->is_survey) {
$this->last_error_message = "This instrument is not a survey";
return null;
}
$url = REDCap::getSurveyLink($record, $this->instrument, $this->event_id, $instance_id, $this->pid);
return $url;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment