Openemr fhir search (#4349)
[openemr.git] / src / RestControllers / RestControllerHelper.php
blobf192e078decde1be7dee7636d9b9340cd2e4cfe8
1 <?php
3 /**
4 * RestControllerHelper
6 * @package OpenEMR
7 * @link http://www.open-emr.org
8 * @author Matthew Vita <matthewvita48@gmail.com>
9 * @copyright Copyright (c) 2018 Matthew Vita <matthewvita48@gmail.com>
10 * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3
13 namespace OpenEMR\RestControllers;
15 use OpenEMR\Services\Search\FhirSearchParameterDefinition;
16 use OpenEMR\Services\Search\SearchFieldType;
17 use OpenEMR\FHIR\R4\FHIRDomainResource\FHIRPatient;
18 use OpenEMR\FHIR\R4\FHIRElement\FHIRCanonical;
19 use OpenEMR\FHIR\R4\FHIRElement\FHIRCode;
20 use OpenEMR\FHIR\R4\FHIRElement\FHIRExtension;
21 use OpenEMR\FHIR\R4\FHIRElement\FHIRRestfulCapabilityMode;
22 use OpenEMR\FHIR\R4\FHIRElement\FHIRTypeRestfulInteraction;
23 use OpenEMR\FHIR\R4\FHIRResource;
24 use OpenEMR\FHIR\R4\FHIRResource\FHIRCapabilityStatement\FHIRCapabilityStatementInteraction;
25 use OpenEMR\FHIR\R4\FHIRResource\FHIRCapabilityStatement\FHIRCapabilityStatementOperation;
26 use OpenEMR\FHIR\R4\FHIRResource\FHIRCapabilityStatement\FHIRCapabilityStatementResource;
27 use OpenEMR\FHIR\R4\FHIRResource\FHIRCapabilityStatement\FHIRCapabilityStatementRest;
28 use OpenEMR\Services\FHIR\IResourceUSCIGProfileService;
29 use OpenEMR\Validators\ProcessingResult;
31 class RestControllerHelper
33 /**
34 * The resource endpoint names we want to skip over.
36 const IGNORE_ENDPOINT_RESOURCES = ['.well-known', 'metadata'];
38 /**
39 * The default FHIR services class namespace
41 const FHIR_SERVICES_NAMESPACE = "OpenEMR\\Services\\FHIR\\Fhir";
43 // @see https://www.hl7.org/fhir/search.html#table
44 const FHIR_SEARCH_CONTROL_PARAM_REV_INCLUDE_PROVENANCE = "Provenance:target";
46 /**
47 * Configures the HTTP status code and payload returned within a response.
49 * @param $serviceResult
50 * @param $customRespPayload
51 * @param $idealStatusCode
52 * @return null
54 public static function responseHandler($serviceResult, $customRespPayload, $idealStatusCode)
56 if ($serviceResult) {
57 http_response_code($idealStatusCode);
59 if ($customRespPayload) {
60 return $customRespPayload;
62 return $serviceResult;
65 // if no result is present return a 404 with a null response
66 http_response_code(404);
67 return null;
70 public static function validationHandler($validationResult)
72 if (property_exists($validationResult, 'isValid') && !$validationResult->isValid()) {
73 http_response_code(400);
74 $validationMessages = null;
75 if (property_exists($validationResult, 'getValidationMessages')) {
76 $validationMessages = $validationResult->getValidationMessages();
77 } else {
78 $validationMessages = $validationResult->getMessages();
80 return $validationMessages;
82 return null;
85 /**
86 * Parses a service processing result for standard Apis to determine the appropriate HTTP status code and response format
87 * for a request.
89 * The response body has a uniform structure with the following top level keys:
90 * - validationErrors
91 * - internalErrors
92 * - data
94 * The response data key conveys the data payload for a response. The payload is either a "top level" array for a
95 * single result, or an array for multiple results.
97 * @param $processingResult - The service processing result.
98 * @param $successStatusCode - The HTTP status code to return for a successful operation that completes without error.
99 * @param $isMultipleResultResponse - Indicates if the response contains multiple results.
100 * @return array[]
102 public static function handleProcessingResult($processingResult, $successStatusCode, $isMultipleResultResponse = false): array
104 $httpResponseBody = [
105 "validationErrors" => [],
106 "internalErrors" => [],
107 "data" => []
109 if (!$processingResult->isValid()) {
110 http_response_code(400);
111 $httpResponseBody["validationErrors"] = $processingResult->getValidationMessages();
112 } elseif ($processingResult->hasInternalErrors()) {
113 http_response_code(500);
114 $httpResponseBody["internalErrors"] = $processingResult->getInternalErrors();
115 } else {
116 http_response_code($successStatusCode);
117 $dataResult = $processingResult->getData();
119 if (!$isMultipleResultResponse) {
120 $dataResult = (count($dataResult) === 0) ? [] : $dataResult[0];
123 $httpResponseBody["data"] = $dataResult;
126 return $httpResponseBody;
130 * Parses a service processing result for FHIR endpoints to determine the appropriate HTTP status code and response format
131 * for a request.
133 * The response body has a normal Fhir Resource json:
135 * @param $processingResult - The service processing result.
136 * @param $successStatusCode - The HTTP status code to return for a successful operation that completes without error.
137 * @return array|mixed
139 public static function handleFhirProcessingResult(ProcessingResult $processingResult, $successStatusCode)
141 $httpResponseBody = [];
142 if (!$processingResult->isValid()) {
143 http_response_code(400);
144 $httpResponseBody["validationErrors"] = $processingResult->getValidationMessages();
145 } elseif (count($processingResult->getData()) <= 0) {
146 http_response_code(404);
147 } elseif ($processingResult->hasInternalErrors()) {
148 http_response_code(500);
149 $httpResponseBody["internalErrors"] = $processingResult->getInternalErrors();
150 } else {
151 http_response_code($successStatusCode);
152 $dataResult = $processingResult->getData();
154 $httpResponseBody = $dataResult[0];
157 return $httpResponseBody;
160 public function setSearchParams($resource, FHIRCapabilityStatementResource $capResource, $service)
162 if (empty($service)) {
163 return; // nothing to do here as the service isn't defined.
165 if (empty($capResource->getSearchInclude())) {
166 $capResource->addSearchInclude('*');
168 if ($service instanceof IResourceUSCIGProfileService && empty($capResource->getSearchRevInclude())) {
169 $capResource->addSearchRevInclude(self::FHIR_SEARCH_CONTROL_PARAM_REV_INCLUDE_PROVENANCE);
171 $searchParams = $service->getSearchParams();
172 $searchParams = is_array($searchParams) ? $searchParams : [];
173 foreach ($searchParams as $fhirSearchField => $searchDefinition) {
176 * @var FhirSearchParameterDefinition $searchDefinition
179 $paramExists = false;
180 $type = $searchDefinition->getType();
182 foreach ($capResource->getSearchParam() as $searchParam) {
183 if (strcmp($searchParam->getName(), $fhirSearchField) == 0) {
184 $paramExists = true;
187 if (!$paramExists) {
188 $param = new FHIRResource\FHIRCapabilityStatement\FHIRCapabilityStatementSearchParam();
189 $param->setName($fhirSearchField);
190 $param->setType($type);
191 $capResource->addSearchParam($param);
198 * Retrieves the fully qualified service class name for a given FHIR resource. It will only return a class that
199 * actually exists.
200 * @param $resource The name of the FHIR resource that we attempt to find the service class for.
201 * @param string $serviceClassNameSpace The namespace to find the class in. Defaults to self::FHIR_SERVICES_NAMESPACE
202 * @return string|null Returns the fully qualified name if the class is found, otherwise it returns null.
204 public function getFullyQualifiedServiceClassForResource($resource, $serviceClassNameSpace = self::FHIR_SERVICES_NAMESPACE)
206 $serviceClass = $serviceClassNameSpace . $resource . "Service";
207 if (class_exists($serviceClass)) {
208 return $serviceClass;
210 return null;
213 public function addOperations($resource, $items, FHIRCapabilityStatementResource $capResource)
215 $operation = end($items);
216 // we want to skip over anything that's not a resource $operation
217 if ($operation === '$export') {
218 $operationName = strtolower($resource) . '-export';
219 // define export operation
220 $resource = new FHIRPatient();
221 $operation = new FHIRCapabilityStatementOperation();
222 $operation->setName($operationName);
223 $operation->setDefinition(new FHIRCanonical('http://hl7.org/fhir/uv/bulkdata/OperationDefinition/' . $operationName));
225 // TODO: adunsulag so the Single Patient API fails on this expectation being here yet the Multi-Patient API failed when it wasn't here
226 // need to investigate what, if anything we are missing, perhaps another extension definition that tells the inferno server
227 // that this should be parsed in a single patient context??
228 // $extension = new FHIRExtension();
229 // $extension->setValueCode(new FHIRCode('SHOULD'));
230 // $extension->setUrl('http://hl7.org/fhir/StructureDefinition/capabilitystatement-expectation');
231 // $operation->addExtension($extension);
232 // $capResource->addOperation($operation);
236 public function addRequestMethods($items, FHIRCapabilityStatementResource $capResource)
238 $reqMethod = trim($items[0], " ");
239 $numberItems = count($items);
240 $code = "";
241 // we want to skip over $export operations.
242 if (end($items) === '$export') {
243 return;
246 // now setup our interaction types
247 if (strcmp($reqMethod, "GET") == 0) {
248 if (!empty(preg_match('/:/', $items[$numberItems - 1]))) {
249 $code = "read";
250 } else {
251 $code = "search-type";
253 } elseif (strcmp($reqMethod, "POST") == 0) {
254 $code = "insert";
255 } elseif (strcmp($reqMethod, "PUT") == 0) {
256 $code = "update";
259 if (!empty($code)) {
260 $interaction = new FHIRCapabilityStatementInteraction();
261 $restfulInteraction = new FHIRTypeRestfulInteraction();
262 $restfulInteraction->setValue($code);
263 $interaction->setCode($restfulInteraction);
264 $capResource->addInteraction($interaction);
269 public function getCapabilityRESTObject($routes, $serviceClassNameSpace = self::FHIR_SERVICES_NAMESPACE, $structureDefinition = "http://hl7.org/fhir/StructureDefinition/"): FHIRCapabilityStatementRest
271 $restItem = new FHIRCapabilityStatementRest();
272 $mode = new FHIRRestfulCapabilityMode();
273 $mode->setValue('server');
274 $restItem->setMode($mode);
276 $resourcesHash = array();
277 foreach ($routes as $key => $function) {
278 $items = explode("/", $key);
279 if ($serviceClassNameSpace == self::FHIR_SERVICES_NAMESPACE) {
280 // FHIR routes always have the resource at $items[2]
281 $resource = $items[2];
282 } else {
283 // API routes do not always have the resource at $items[2]
284 if (count($items) < 5) {
285 $resource = $items[2];
286 } elseif (count($items) < 7) {
287 $resource = $items[4];
288 if (substr($resource, 0, 1) === ':') {
289 // special behavior needed for the API portal route
290 $resource = $items[3];
292 } else { // count($items) < 9
293 $resource = $items[6];
297 if (!in_array($resource, self::IGNORE_ENDPOINT_RESOURCES)) {
298 $service = null;
299 $serviceClass = $this->getFullyQualifiedServiceClassForResource($resource, $serviceClassNameSpace);
300 if (!empty($serviceClass)) {
301 $service = new $serviceClass();
303 if (!array_key_exists($resource, $resourcesHash)) {
304 $capResource = new FHIRCapabilityStatementResource();
305 $capResource->setType(new FHIRCode($resource));
306 $capResource->setProfile(new FHIRCanonical($structureDefinition . $resource));
308 if ($service instanceof IResourceUSCIGProfileService) {
309 $profileUris = $service->getProfileURIs();
310 foreach ($profileUris as $uri) {
311 $capResource->addSupportedProfile(new FHIRCanonical($uri));
314 $resourcesHash[$resource] = $capResource;
316 $this->setSearchParams($resource, $resourcesHash[$resource], $service);
317 $this->addRequestMethods($items, $resourcesHash[$resource]);
318 $this->addOperations($resource, $items, $resourcesHash[$resource]);
322 foreach ($resourcesHash as $resource => $capResource) {
323 $restItem->addResource($capResource);
325 return $restItem;