PHPCap Docs | PHPCap API
Overview

Namespaces

  • IU
    • PHPCap
  • PHP

Classes

  • IU\PHPCap\ErrorHandler
  • IU\PHPCap\FileUtil
  • IU\PHPCap\RedCap
  • IU\PHPCap\RedCapApiConnection
  • IU\PHPCap\RedCapProject

Interfaces

  • IU\PHPCap\ErrorHandlerInterface
  • IU\PHPCap\RedCapApiConnectionInterface

Exceptions

  • Exception
  • IU\PHPCap\PhpCapException
  • Overview
  • Namespace
  • Class
  1:   2:   3:   4:   5:   6:   7:   8:   9:  10:  11:  12:  13:  14:  15:  16:  17:  18:  19:  20:  21:  22:  23:  24:  25:  26:  27:  28:  29:  30:  31:  32:  33:  34:  35:  36:  37:  38:  39:  40:  41:  42:  43:  44:  45:  46:  47:  48:  49:  50:  51:  52:  53:  54:  55:  56:  57:  58:  59:  60:  61:  62:  63:  64:  65:  66:  67:  68:  69:  70:  71:  72:  73:  74:  75:  76:  77:  78:  79:  80:  81:  82:  83:  84:  85:  86:  87:  88:  89:  90:  91:  92:  93:  94:  95:  96:  97:  98:  99: 100: 101: 102: 103: 104: 105: 106: 107: 108: 109: 110: 111: 112: 113: 114: 115: 116: 117: 118: 119: 120: 121: 122: 123: 124: 125: 126: 127: 128: 129: 130: 131: 132: 133: 134: 135: 136: 137: 138: 139: 140: 141: 142: 143: 144: 145: 146: 147: 148: 149: 150: 151: 152: 153: 154: 155: 156: 157: 158: 159: 160: 161: 162: 163: 164: 165: 166: 167: 168: 169: 170: 171: 172: 173: 174: 175: 176: 177: 178: 179: 180: 181: 182: 183: 184: 185: 186: 187: 188: 189: 190: 191: 192: 193: 194: 195: 196: 197: 198: 199: 200: 201: 202: 203: 204: 205: 206: 207: 208: 209: 210: 211: 212: 213: 214: 215: 216: 217: 218: 219: 220: 221: 222: 223: 224: 225: 226: 227: 228: 229: 230: 231: 232: 233: 234: 235: 236: 237: 238: 239: 240: 241: 242: 243: 244: 245: 246: 247: 248: 249: 250: 251: 252: 253: 254: 255: 256: 257: 258: 259: 260: 261: 262: 263: 264: 265: 266: 267: 268: 269: 270: 271: 272: 273: 274: 275: 276: 277: 278: 279: 280: 281: 282: 283: 284: 285: 286: 287: 288: 289: 290: 291: 292: 293: 294: 295: 296: 297: 298: 299: 300: 301: 302: 303: 304: 305: 306: 307: 308: 309: 310: 311: 312: 313: 314: 315: 316: 317: 318: 319: 320: 321: 322: 323: 324: 325: 326: 327: 328: 329: 330: 331: 332: 333: 334: 335: 336: 337: 338: 339: 340: 341: 342: 343: 344: 345: 346: 347: 348: 349: 350: 351: 352: 353: 354: 355: 356: 357: 358: 359: 360: 361: 362: 363: 364: 365: 366: 367: 368: 369: 370: 371: 372: 373: 374: 375: 376: 377: 378: 379: 380: 381: 382: 383: 384: 385: 386: 387: 388: 389: 390: 391: 392: 393: 394: 395: 396: 397: 398: 399: 400: 401: 402: 403: 404: 405: 406: 407: 408: 409: 410: 411: 412: 413: 414: 415: 416: 417: 418: 419: 420: 421: 422: 423: 424: 425: 426: 427: 428: 429: 430: 431: 432: 433: 434: 435: 436: 437: 438: 439: 440: 441: 442: 443: 444: 445: 446: 447: 448: 449: 450: 451: 452: 453: 454: 455: 456: 457: 458: 459: 460: 461: 462: 463: 464: 465: 466: 467: 468: 469: 470: 471: 472: 473: 474: 475: 476: 477: 478: 479: 480: 481: 482: 483: 484: 485: 486: 487: 488: 489: 490: 491: 492: 493: 494: 495: 496: 497: 498: 499: 500: 501: 502: 
<?php

namespace IU\PHPCap;

/**
 * REDCap class used to represent a REDCap instance/site. This class
 * is typically only useful if your progam needs to create
 * REDCap projects and/or needs to access more than one
 * REDCap project.
 */
class RedCap
{
    protected $superToken;
    
    protected $errorHandler;
    
    /** connection to the REDCap API at the $apiURL. */
    protected $connection;

    /** function for creating project object */
    protected $ProjectConstructorCallback;
    
    /**
     *
     * @param string $apiUrl the URL for the API for your REDCap site.
     * @param string $superToken the user's super token. This needs to be provided if
     *     you are going to create projects.
     * @param boolean $sslVerify indicates if SSL connection to REDCap web site should be verified.
     * @param string $caCertificateFile the full path name of the CA (Certificate Authority)
     *     certificate file.
     * @param ErrorHandlerInterface $errorHandler the error handler that will be
     *    used. This would normally only be set if you want to override the PHPCap's default
     *    error handler.
     * @param RedCapApiConnectionInterface $connection the connection that will be used.
     *    This would normally only be set if you want to override the PHPCap's default
     *    connection. If this argument is specified, the $apiUrl, $sslVerify, and
     *    $caCertificateFile arguments will be ignored, and the values for these
     *    set in the connection will be used.
     */
    public function __construct(
        $apiUrl,
        $superToken = null,
        $sslVerify = false,
        $caCertificateFile = null,
        $errorHandler = null,
        $connection = null
    ) {
        # Need to set errorHandler to default to start in case there is an
        # error with the errorHandler passed as an argument
        # (to be able to handle that error!)
        $this->errorHandler = new ErrorHandler();
        if (isset($errorHandler)) {
            $this->errorHandler = $this->processErrorHandlerArgument($errorHandler);
        } // @codeCoverageIgnore
    
        if (isset($connection)) {
            $this->connection = $this->processConnectionArgument($connection);
        } else {
            $apiUrl    = $this->processApiUrlArgument($apiUrl);
            $sslVerify = $this->processSslVerifyArgument($sslVerify);
            $caCertificateFile = $this->processCaCertificateFileArgument($caCertificateFile);
        
            $this->connection = new RedCapApiConnection($apiUrl, $sslVerify, $caCertificateFile);
        }
        
        $this->superToken = $this->processSuperTokenArgument($superToken);
        
        $this->ProjectConstructorCallback = function (
            $apiUrl,
            $apiToken,
            $sslVerify = false,
            $caCertificateFile = null,
            $errorHandler = null,
            $connection = null
        ) {
            return new RedCapProject(
                $apiUrl,
                $apiToken,
                $sslVerify,
                $caCertificateFile,
                $errorHandler,
                $connection
            );
        };
    }

    
 
    /**
     * Creates a REDCap project with the specified data.
     *
     * The data fields that can be set are as follows:
     * <ul>
     *   <li>
     *     <b>project_title</b> - the title of the project.
     *   </li>
     *   <li>
     *     <b>purpose</b> - the purpose of the project:
     *     <ul>
     *       <li>0 - Practice/Just for fun</li>
     *       <li>1 - Other</li>
     *       <li>2 - Research</li>
     *       <li>3 - Quality Improvement</li>
     *       <li>4 - Operational Support</li>
     *     </ul>
     *   </li>
     *   <li>
     *     <b>purpose_other</b> - text descibing purpose if purpose above is specified as 1.
     *   </li>
     *   <li>
     *     <b>project_notes</b> - notes about the project.
     *   </li>
     *   <li>
     *     <b>is_longitudinal</b> - indicates if the project is longitudinal (0 = False [default],
     *     1 = True).
     *   </li>
     *   <li>
     *     <b>surveys_enabled</b> - indicates if surveys are enabled (0 = False [default], 1 = True).
     *   </li>
     *   <li>
     *     <b>record_autonumbering_enabled</b> - indicates id record autonumbering is enabled
     *     (0 = False [default], 1 = True).
     *   </li>
     * </ul>
     *
     * @param mixed $projectData the data used for project creation. Note that if
     *     'php' format is used, the data needs to be an array where the keys are
     *     the field names and the values are the field values.
     * @param $format string the format used to export the arm data.
     *     <ul>
     *       <li> 'php' - [default] array of maps of values</li>
     *       <li> 'csv' - string of CSV (comma-separated values)</li>
     *       <li> 'json' - string of JSON encoded values</li>
     *       <li> 'xml' - string of XML encoded data</li>
     *     </ul>
     * @param string $odm
     * @return RedCapProject the project that was created.
     */
    public function createProject(
        $projectData,
        $format = 'php',
        $odm = null
    ) {
        // Note: might want to clone error handler, in case state variables
        // have been added that should differ for different uses, e.g.,
        // a user message that is displayed where you have multiple project
        // objects
        $data = array(
                'token'        => $this->superToken,
                'content'      => 'project',
                'returnFormat' => 'json'
        );
        
        #---------------------------------------------
        # Process the arguments
        #---------------------------------------------
        $legalFormats = array('csv', 'json', 'php', 'xml');
        $data['format'] = $this->processFormatArgument($format, $legalFormats);
        $data['data']   = $this->processImportDataArgument($projectData, 'projectData', $format);
        
        if (isset($odm)) {
            $data['odm'] = $this->processOdmArgument($odm);
        }
        
        #---------------------------------------
        # Create the project
        #---------------------------------------
        $apiToken = $this->connection->callWithArray($data);
        
        $this->processNonExportResult($apiToken);
        
        $connection   = clone $this->connection;
        $errorHandler = clone $this->errorHandler;
        
        $ProjectConstructorCallback = $this->ProjectConstructorCallback;
        
        $project = call_user_func(
            $ProjectConstructorCallback,
            $apiUrl = null,
            $apiToken,
            $sslVerify = null,
            $caCertificateFile = null,
            $errorHandler,
            $connection
        );
        
        return $project;
    }
    
    /**
     * Gets the REDCap project for the specified API token.
     *
     * @param string $apiToken the API token for the project to get.
     *
     * @return \IU\PHPCap\RedCapProject the project for the specified API token.
     */
    public function getProject($apiToken)
    {
        $apiToken = $this->processApiTokenArgument($apiToken);
        
        $connection   = clone $this->connection;
        $errorHandler = clone $this->errorHandler;
        
        $ProjectConstructorCallback = $this->ProjectConstructorCallback;
        
        # By default, this creates a RedCapProject
        $project = call_user_func(
            $ProjectConstructorCallback,
            $apiUrl = null,
            $apiToken,
            $sslVerify = null,
            $caCertificateFile = null,
            $errorHandler,
            $connection
        );
        
        return $project;
    }
    
    /**
     * Gets the function used to create projects.
     *
     * @return callable the function used by this class to create projects.
     */
    public function getProjectConstructorCallback()
    {
        return $this->ProjectConstructorCallback;
    }
    
    /**
     * Sets the function used to create projects in this class.
     * This method would normally only be used if you have extended
     * the RedCapProject class and want RedCap to return
     * projects using your extended class.
     *
     * @param callable $ProjectConstructorCallback the function to call to create a new project.
     *     The function will be passed the same arguments as the RedCapProject
     *     constructor gets.
     */
    public function setProjectConstructorCallback($ProjectConstructorCallback)
    {
        $this->ProjectConstructorCallback
            = $this->processProjectConstructorCallbackArgument($ProjectConstructorCallback);
    }
    
    /**
     * Gets the error handler being used.
     *
     * @return ErrorHandlerInterface the error handler being used.
     */
    public function getErrorHandler()
    {
        return $this->errorHandler;
    }
    
    /**
     * Set the error handler that is used.
     *
     * @param ErrorHandlerInterface $errorHandler the error handler to use.
     */
    public function setErrorHandler($errorHandler)
    {
        $this->errorHandler = $this->processErrorHandlerArgument($errorHandler);
    }
    
    /**
     * Gets the connection being used.
     *
     * @return RedCapApiConnectionInterface the connection being used.
     */
    public function getConnection()
    {
        return $this->connection;
    }
    
    /**
     * Sets the connection that is used.
     *
     * @param RedCapApiConnectionInterface $connection the connection to use.
     */
    public function setConnection($connection)
    {
        $this->connection = $this->processConnectionArgument($connection);
    }

    
    protected function processApiTokenArgument($apiToken)
    {
        if (!isset($apiToken)) {
            $message = 'The REDCap API token specified for the project was null or blank.';
            $code    =  ErrorHandlerInterface::INVALID_ARGUMENT;
            $this->errorHandler->throwException($message, $code);
        } elseif (gettype($apiToken) !== 'string') {
            $message = 'The REDCap API token provided should be a string, but has type: '
                .gettype($apiToken);
            $code =  ErrorHandlerInterface::INVALID_ARGUMENT;
            $this->errorHandler->throwException($message, $code);
        } elseif (!ctype_xdigit($apiToken)) {   // ctype_xdigit - check token for hexidecimal
            $message = 'The REDCap API token has an invalid format.'
                .' It should only contain numbers and the letters A, B, C, D, E and F.';
            $code = ErrorHandlerInterface::INVALID_ARGUMENT;
            $this->errorHandler->throwException($message, $code);
        } elseif (strlen($apiToken) != 32) { # Note: super tokens are not valid for project methods
            $message = 'The REDCap API token has an invalid format.'
                .' It has a length of '.strlen($apiToken).' characters, but should have a length of'
                .' 32.';
            $code = ErrorHandlerInterface::INVALID_ARGUMENT;
            $this->errorHandler->throwException($message, $code);
        } // @codeCoverageIgnore
        return $apiToken;
    }
    
    
    protected function processApiUrlArgument($apiUrl)
    {
        # Note: standard PHP URL validation will fail for non-ASCII URLs (so it was not used)
        if (!isset($apiUrl)) {
            $message = 'The REDCap API URL specified for the project was null or blank.';
            $code    = ErrorHandlerInterface::INVALID_ARGUMENT;
            $this->errorHandler->throwException($message, $code);
        } elseif (gettype($apiUrl) !== 'string') {
            $message = 'The REDCap API URL provided ('.$apiUrl.') should be a string, but has type: '
                . gettype($apiUrl);
            $code = ErrorHandlerInterface::INVALID_ARGUMENT;
            $this->errorHandler->throwException($message, $code);
        } // @codeCoverageIgnore
        return $apiUrl;
    }
    
    
    protected function processCaCertificateFileArgument($caCertificateFile)
    {
        if (isset($caCertificateFile) && gettype($caCertificateFile) !== 'string') {
            $message = 'The value for $sslVerify must be a string, but has type: '
                .gettype($caCertificateFile);
            $code    = ErrorHandlerInterface::INVALID_ARGUMENT;
            $this->errorHandler->throwException($message, $code);
        } // @codeCoverageIgnore
        return $caCertificateFile;
    }
    
    protected function processConnectionArgument($connection)
    {
        if (!($connection instanceof RedCapApiConnectionInterface)) {
            $message = 'The connection argument is not valid, because it doesn\'t implement '
                .RedCapApiConnectionInterface::class.'.';
            $code = ErrorHandlerInterface::INVALID_ARGUMENT;
            $this->errorHandler->throwException($message, $code);
        } // @codeCoverageIgnore
        return $connection;
    }
    
    protected function processErrorHandlerArgument($errorHandler)
    {
        if (!($errorHandler instanceof ErrorHandlerInterface)) {
            $message = 'The error handler argument is not valid, because it doesn\'t implement '
                .ErrorHandlerInterface::class.'.';
            $code = ErrorHandlerInterface::INVALID_ARGUMENT;
            $this->errorHandler->throwException($message, $code);
        } // @codeCoverageIgnore
        return $errorHandler;
    }
    
    protected function processFormatArgument(& $format, $legalFormats)
    {
        if (gettype($format) !== 'string') {
            $message = 'The format specified has type "'.gettype($format).'", but it should be a string.';
            $this->errorHandler->throwException($message, ErrorHandlerInterface::INVALID_ARGUMENT);
        } // @codeCoverageIgnore
        
        $format = strtolower(trim($format));
        
        if (!in_array($format, $legalFormats)) {
            $message = 'Invalid format "'.$format.'" specified.'
                .' The format should be one of the following: "'.
                implode('", "', $legalFormats).'".';
            $this->errorHandler->throwException($message, ErrorHandlerInterface::INVALID_ARGUMENT);
        } // @codeCoverageIgnore
        
        $dataFormat = '';
        if (strcmp($format, 'php') === 0) {
            $dataFormat = 'json';
        } else {
            $dataFormat = $format;
        }
        
        return $dataFormat;
    }
    
    protected function processImportDataArgument($data, $dataName, $format)
    {
        if (!isset($data)) {
            $message = "No value specified for required argument '".$dataName."'.";
            $this->errorHandler->throwException($message, ErrorHandlerInterface::INVALID_ARGUMENT);
        } elseif ($format === 'php') {
            if (!is_array($data)) {
                $message = "Argument '".$dataName."' has type '".gettype($data)."'"
                    .", but should be an array.";
                    $this->errorHandler->throwException($message, ErrorHandlerInterface::INVALID_ARGUMENT);
            } // @codeCoverageIgnore
            $data = array($data); // Needs to be an array within an array to work
            $data = json_encode($data);
            
            $jsonError = json_last_error();
            
            switch ($jsonError) {
                case JSON_ERROR_NONE:
                    break;
                default:
                    $message =  'JSON error ('.$jsonError.') "'. json_last_error_msg().
                    '"'." while processing argument '".$dataName."'.";
                    $this->errorHandler->throwException($message, ErrorHandlerInterface::JSON_ERROR);
                    break; // @codeCoverageIgnore
            }
        } else { // @codeCoverageIgnore
            // All non-php formats:
            if (gettype($data) !== 'string') {
                $message = "Argument '".$dataName."' has type '".gettype($data)."'"
                    .", but should be a string.";
                $this->errorHandler->throwException($message, ErrorHandlerInterface::INVALID_ARGUMENT);
            } // @codeCoverageIgnore
        }
        
        return $data;
    }
    
    protected function processNonExportResult(& $result)
    {
        $matches = array();
        $hasMatch = preg_match('/^[\s]*{"error":\s*"([^"]+)"}[\s]*$/', $result, $matches);
        if ($hasMatch === 1) {
            // note: $matches[0] is the complete string that matched
            //       $matches[1] is just the error message part
            $message = $matches[1];
            $this->errorHandler->throwException($message, ErrorHandlerInterface::REDCAP_API_ERROR);
        } // @codeCoverageIgnore
    }
    
    
    protected function processOdmArgument($odm)
    {
        if (isset($odm) && !is_string($odm)) {
            $message = 'The value for $odm must have type string, but has type: '
                .gettype($odm);
                $code = ErrorHandlerInterface::INVALID_ARGUMENT;
                $this->errorHandler->throwException($message, $code);
        } // @codeCoverageIgnore
        
        return $odm;
    }
    
    protected function processProjectConstructorCallbackArgument($callback)
    {
        if (!is_callable($callback)) {
            $message = 'The project constructor callback needs to be callable (i.e., be a function)'
                .', but it isn\'t.';
            $code = ErrorHandlerInterface::INVALID_ARGUMENT;
            $this->errorHandler->throwException($message, $code);
        } // @codeCoverageIgnore
        
        return $callback;
    }
    
    protected function processSslVerifyArgument($sslVerify)
    {
        if (isset($sslVerify) && gettype($sslVerify) !== 'boolean') {
            $message = 'The value for $sslVerify must be a boolean (true/false), but has type: '
                .gettype($sslVerify);
            $code = ErrorHandlerInterface::INVALID_ARGUMENT;
            $this->errorHandler->throwException($message, $code);
        } // @codeCoverageIgnore
        
        return $sslVerify;
    }
    
    
    protected function processSuperTokenArgument($superToken)
    {
        if (!isset($superToken) || trim($superToken) === '') {
            ;  // OK; just means that createProject can't be used
        } elseif (gettype($superToken) !== 'string') {
            $this->errorHandler->throwException("The REDCap super token provided should be a string, but has type: "
                . gettype($superToken), ErrorHandlerInterface::INVALID_ARGUMENT);
        } elseif (!ctype_xdigit($superToken)) {   // ctype_xdigit - check token for hexidecimal
            $this->errorHandler->throwException(
                "The REDCap super token has an invalid format."
                ." It should only contain numbers and the letters A, B, C, D, E and F.",
                ErrorHandlerInterface::INVALID_ARGUMENT
            );
        } elseif (strlen($superToken) != 64) {
            $this->errorHandler->throwException(
                "The REDCap super token has an invalid format."
                . " It has a length of ".strlen($superToken)." characters, but should have a length of"
                . " 64 characters.",
                ErrorHandlerInterface::INVALID_ARGUMENT
            );
        } // @codeCoverageIgnore
        
        return $superToken;
    }
}
PHPCap API documentation generated by ApiGen