psr12 fixes for new PHP_CodeSniffer (#4795)
[openemr.git] / src / Services / ProcedureService.php
blobe88e6adc2768ab9aa8512bf4083ba06c2465d954
1 <?php
3 /**
4 * ProcedureService
6 * @package OpenEMR
7 * @link http://www.open-emr.org
8 * @author Yash Bothra <yashrajbothra786gmail.com>
9 * @copyright Copyright (c) 2020 Yash Bothra <yashrajbothra786gmail.com>
10 * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3
13 namespace OpenEMR\Services;
15 use OpenEMR\Common\Database\QueryUtils;
16 use OpenEMR\Common\Uuid\UuidRegistry;
17 use OpenEMR\Services\Search\CompositeSearchField;
18 use OpenEMR\Services\Search\FhirSearchWhereClauseBuilder;
19 use OpenEMR\Services\Search\ISearchField;
20 use OpenEMR\Services\Search\SearchModifier;
21 use OpenEMR\Services\Search\StringSearchField;
22 use OpenEMR\Services\Search\TokenSearchField;
23 use OpenEMR\Services\Search\TokenSearchValue;
24 use OpenEMR\Validators\BaseValidator;
25 use OpenEMR\Validators\ProcessingResult;
27 class ProcedureService extends BaseService
29 private const PROCEDURE_TABLE = "procedure_order";
30 private const PATIENT_TABLE = "patient_data";
31 private const ENCOUNTER_TABLE = "form_encounter";
32 private const PRACTITIONER_TABLE = "users";
33 private const PROCEDURE_PROVIDER_TABLE = "procedure_providers";
34 private const PROCEDURE_REPORT_TABLE = "procedure_report";
35 private const PROCEDURE_RESULT_TABLE = "procedure_result";
37 /**
38 * Default constructor.
40 public function __construct()
42 parent::__construct(self::PROCEDURE_TABLE);
43 UuidRegistry::createMissingUuidsForTables([self::PROCEDURE_TABLE, self::PATIENT_TABLE, self::ENCOUNTER_TABLE
44 , self::PRACTITIONER_TABLE, self::PROCEDURE_REPORT_TABLE, self::PROCEDURE_PROVIDER_TABLE
45 , self::PROCEDURE_RESULT_TABLE]);
48 public function getUuidFields(): array
50 return ['result_uuid','report_uuid', 'lab_uuid','puuid', 'order_uuid', 'euuid', 'provider_uuid'];
53 public function search($search, $isAndCondition = true)
55 // note that these are Laboratory tests & values/results as mapped in USCDI Data elements v1
56 // @see https://www.healthit.gov/isa/sites/isa/files/2020-07/USCDI-Version-1-July-2020-Errata-Final.pdf
57 // To see the mappings you can see here: https://www.hl7.org/fhir/us/core/general-guidance.html
58 $sql = "SELECT
59 porder.order_uuid
60 ,porder.order_provider_id
61 ,porder.order_activity
62 ,porder.order_diagnosis
63 ,porder.order_encounter_id
64 ,porder.order_lab_id
66 ,preport.report_date
67 ,preport.procedure_report_id
68 ,preport.report_uuid
69 ,preport.report_notes
71 ,presult.procedure_result_id
72 ,presult.result_uuid
73 ,presult.result_code
74 ,presult.result_text
75 ,presult.result_units
76 ,presult.result_result
77 ,presult.result_range
78 ,presult.result_abnormal
79 ,presult.result_comments
80 ,presult.result_status
82 ,order_codes.procedure_name
83 ,order_codes.procedure_code
84 ,order_codes.procedure_type
86 ,pcode_types.standard_code
88 ,labs.lab_id
89 ,labs.lab_uuid
90 ,labs.lab_npi
91 ,labs.lab_name
93 ,patients.puuid
94 ,patients.pid
96 ,encounters.eid
97 ,encounters.euuid
98 ,encounters.encounter_date
100 ,docs.doc_id
101 ,docs.doc_uuid
103 ,provider.provider_uuid
104 ,provider.provider_id
105 ,provider.provider_fname
106 ,provider.provider_mname
107 ,provider.provider_lname
108 ,provider.provider_npi
109 FROM (
110 SELECT
111 date_report AS report_date
112 ,procedure_report_id
113 ,procedure_order_id
114 ,procedure_order_seq
115 ,uuid AS report_uuid
116 ,report_notes
117 FROM
118 procedure_report
119 ) preport
120 LEFT JOIN (
121 SELECT
122 procedure_result_id
123 ,procedure_report_id
124 ,uuid AS result_uuid
125 ,result AS result_quantity
126 ,result AS result_string
127 ,result AS result_result
128 ,units AS result_units
129 ,result_status
130 ,result_code
131 ,result_text
132 ,result_data_type
133 ,`range` AS result_range
134 ,`abnormal` AS result_abnormal
135 ,`comments` AS result_comments
136 ,`document_id` AS result_document_id
137 FROM
138 `procedure_result`
139 ) presult
141 preport.procedure_report_id = presult.procedure_report_id
142 LEFT JOIN (
143 SELECT
144 procedure_order_id
145 ,uuid AS order_uuid
146 ,provider_id AS order_provider_id
147 ,encounter_id AS order_encounter_id
148 ,activity AS order_activity
149 ,order_diagnosis
150 ,lab_id as order_lab_id
151 ,procedure_order_id AS order_id
152 ,patient_id AS order_patient_id
153 ,provider_id
154 FROM
155 procedure_order
156 ) porder
158 porder.procedure_order_id = preport.procedure_order_id
159 LEFT JOIN
161 select
162 encounter AS eid
163 ,uuid AS euuid
164 ,`date` AS encounter_date
165 FROM
166 form_encounter
167 ) encounters ON porder.order_encounter_id = encounters.eid
168 LEFT JOIN
170 SELECT
171 ppid AS lab_id
172 ,uuid AS lab_uuid
173 ,npi AS lab_npi
174 ,`name` AS lab_name
175 ,`active` AS lab_active
176 FROM
177 procedure_providers
178 ) labs
180 labs.lab_id = porder.order_lab_id
181 LEFT JOIN
183 select
184 procedure_order_id
185 ,procedure_order_seq
186 ,procedure_code
187 ,procedure_name
188 -- we exclude the legacy procedure_type and use procedure_order_title
189 ,procedure_order_title AS procedure_type
190 FROM procedure_order_code
192 order_codes
194 order_codes.procedure_order_id = porder.procedure_order_id AND order_codes.procedure_order_seq = preport.procedure_order_seq
195 LEFT JOIN (
196 select
197 standard_code,
198 procedure_code AS proc_code
199 FROM procedure_type
200 ) pcode_types ON order_codes.procedure_code = pcode_types.proc_code
201 LEFT JOIN (
202 select
204 ,uuid AS puuid
205 FROM
206 patient_data
207 ) patients
209 patients.pid = porder.order_patient_id
211 LEFT JOIN (
212 select
213 id AS doc_id
214 ,uuid AS doc_uuid
215 FROM
216 documents
217 ) docs ON presult.result_document_id = docs.doc_id
218 LEFT JOIN (
219 SELECT
220 users.uuid AS provider_uuid
221 ,users.id AS provider_id
222 ,users.fname AS provider_fname
223 ,users.mname AS provider_mname
224 ,users.lname AS provider_lname
225 ,users.npi AS provider_npi
226 FROM users
227 WHERE npi IS NOT NULL AND npi != ''
228 ) provider ON provider.provider_id = porder.provider_id ";
230 $excludeDNR_TNP = new StringSearchField('result_string', ['DNR','TNP'], SearchModifier::NOT_EQUALS_EXACT, true);
231 if (isset($search['result_string']) && $search['result_string'] instanceof ISearchField) {
232 $compoundColumn = new CompositeSearchField('result_string', [], true);
233 $compoundColumn->addChild($search['result_string']);
234 $compoundColumn->addChild($excludeDNR_TNP);
235 $search['result_string'] = $compoundColumn;
236 } else {
237 $compoundColumn = new CompositeSearchField('result_string', [], false);
238 // we have to have an optional is null due to the way the joins are setup.
239 $resultIsNull = new TokenSearchField('result_string', [new TokenSearchValue(true)]);
240 $resultIsNull->setModifier(SearchModifier::MISSING);
241 $compoundColumn->addChild($resultIsNull);
242 $compoundColumn->addChild($excludeDNR_TNP);
243 $search['result_string'] = $compoundColumn;
246 $whereClause = FhirSearchWhereClauseBuilder::build($search, $isAndCondition);
248 $sql .= $whereClause->getFragment();
249 $sqlBindArray = $whereClause->getBoundValues();
250 $statementResults = QueryUtils::sqlStatementThrowException($sql, $sqlBindArray);
252 $processingResult = $this->hydrateSearchResultsFromQueryResource($statementResults);
253 return $processingResult;
256 public function searchProcedureReports($search, $isAndCondition)
258 $query = "SELECT CONCAT_WS('',po.procedure_order_id,poc.`procedure_order_seq`) AS tcode,
259 prs.result AS result_value,
260 prs.units, prs.range,
261 poc.procedure_name AS order_title,
262 prs.result_code as result_code,
263 prs.result_text as result_desc,
264 po.date_ordered,
265 prs.date AS result_time,
266 prs.abnormal AS abnormal_flag,
267 prs.procedure_result_id AS result_id
268 FROM procedure_order AS po
269 JOIN procedure_order_code AS poc ON poc.`procedure_order_id`=po.`procedure_order_id`
270 JOIN procedure_report AS pr ON pr.procedure_order_id = po.procedure_order_id
271 AND pr.`procedure_order_seq`=poc.`procedure_order_seq`
272 JOIN procedure_result AS prs ON prs.procedure_report_id = pr.procedure_report_id
273 WHERE po.patient_id = ? AND prs.result NOT IN ('DNR','TNP')";
276 private function hydrateSearchResultsFromQueryResource($queryResource)
278 $processingResult = new ProcessingResult();
279 $procedureByUuid = [];
280 $reportsByUuid = [];
281 $procedures = [];
282 while ($row = sqlFetchArray($queryResource)) {
283 $record = $this->createResultRecordFromDatabaseResult($row);
284 $procedureUuid = $record['order_uuid'];
285 if (!isset($procedureByUuid[$procedureUuid])) {
286 // setup the table here
287 $procedure = [
288 'name' => $record['procedure_name']
289 ,'uuid' => $record['order_uuid']
290 , 'code' => $record['procedure_code']
291 , 'standard_code' => $record['standard_code']
292 , 'diagnosis' => $record['order_diagnosis']
293 , 'activity' => $record['order_activity']
295 , 'reports' => []
297 if (!empty($record['provider_id'])) {
298 $procedure['provider'] = [
299 'id' => $record['provider_id']
300 ,'uuid' => $record['provider_uuid']
301 ,'fname' => $record['fname']
302 ,'mname' => $record['mname']
303 ,'lname' => $record['lname']
304 ,'npi' => $record['npi']
307 if (!empty($record['lab_id'])) {
308 $procedure['lab'] = [
309 'id' => $record['lab_id'] ?? null
310 ,'uuid' => $record['lab_uuid'] ?? null
311 ,'name' => $record['lab_name'] ?? null
312 ,'npi' => $record['lab_npi'] ?? null
315 if (!empty($record['pid'])) {
316 $procedure['patient'] = [
317 'pid' => $record['pid']
318 ,'uuid' => $record['puuid']
321 if (!empty($record['eid'])) {
322 $procedure['encounter'] = [
323 'id' => $record['eid']
324 ,'uuid' => $record['euuid']
325 ,'date' => $record['encounter_date']
328 $procedures[] = $procedureUuid;
329 } else {
330 $procedure = $procedureByUuid[$procedureUuid];
333 $reportUuid = $record['report_uuid'];
334 if (!isset($reportsByUuid[$reportUuid])) {
335 $report = [
336 'date' => $record['report_date']
337 , 'id' => $record['procedure_report_id']
338 , 'uuid' => $record['report_uuid']
339 , 'notes' => $record['report_notes']
340 , 'results' => []
342 $procedure['reports'][] = $reportUuid;
344 // now add our result
346 * presult.procedure_result_id
347 * ,presult.uuid
348 * ,presult.result_code
349 * ,presult.result_text
350 * ,presult.units
351 * ,presult.result
352 * ,presult.range
353 * ,presult.abnormal
354 * ,presult.comments
355 * ,presult.document_id
356 * ,presult.result_status
358 if (!empty($record['procedure_result_id'])) {
359 $result = [
360 'id' => $record['procedure_result_id']
361 , 'uuid' => $record['result_uuid']
362 , 'code' => $record['result_code']
363 , 'text' => $record['result_text']
364 , 'units' => $record['result_units']
365 , 'result' => $record['result_result']
366 , 'range' => $record['result_range']
367 , 'abnormal' => $record['result_abnormal']
368 , 'comments' => $record['result_comments']
369 , 'document_id' => $record['result_document_id']
370 , 'status' => $record['result_status']
372 $report['results'][] = $result;
374 // need to copy back in since we don't have a copy by reference here
375 $reportsByUuid[$reportUuid] = $report;
376 $procedureByUuid[$procedureUuid] = $procedure;
380 // now go through our ordered list of procedures and let's map the procedure uuids to the actual procedures
381 // map each of the procedure report uuids to their corresponding report arrays
382 // TODO: if we want to optimize all of this for memory efficiency we should probably use objects instead of arrays
383 // that way we can eliminate the implicy copy by reference of php arrays. This may not be a problem in future
384 // versions of php.
385 foreach ($procedures as $uuid) {
386 $procedure = $procedureByUuid[$uuid];
387 $procedure['reports'] = array_map(function ($reportUuid) use ($reportsByUuid) {
388 return $reportsByUuid[$reportUuid];
389 }, $procedure['reports']);
390 $processingResult->addData($procedure);
392 return $processingResult;
395 public function createResultRecordFromDatabaseResult($row)
398 return parent::createResultRecordFromDatabaseResult($row); // TODO: Change the autogenerated stub
402 * Returns a list of procedures matching optional search criteria.
403 * Search criteria is conveyed by array where key = field/column name, value = field value.
404 * If no search criteria is provided, all records are returned.
406 * @param $search search array parameters
407 * @param $isAndCondition specifies if AND condition is used for multiple criteria. Defaults to true.
408 * @param $puuidBind - Optional variable to only allow visibility of the patient with this puuid.
409 * @return ProcessingResult which contains validation messages, internal error messages, and the data
410 * payload.
412 public function getAll($search = array(), $isAndCondition = true, $puuidBind = null)
414 $sqlBindArray = array();
416 if (isset($search['patient.uuid'])) {
417 $isValidPatient = BaseValidator::validateId(
418 'uuid',
419 self::PATIENT_TABLE,
420 $search['patient.uuid'],
421 true
423 if ($isValidPatient !== true) {
424 return $isValidPatient;
426 $search['patient.uuid'] = UuidRegistry::uuidToBytes($search['patient.uuid']);
429 if (!empty($puuidBind)) {
430 // code to support patient binding
431 $isValidPatient = BaseValidator::validateId(
432 'uuid',
433 self::PATIENT_TABLE,
434 $puuidBind,
435 true
437 if ($isValidPatient !== true) {
438 return $isValidPatient;
442 $sql = "SELECT porder.*,
443 pcode.diagnoses,
444 pcode.procedure_order_title,
445 pcode.procedure_name,
446 pcode.procedure_code,
447 presult.result_status,
448 presult.result_code,
449 presult.result_text,
450 presult.date,
451 presult.facility,
452 presult.units,
453 presult.result,
454 patient.uuid AS puuid,
455 encounter.uuid AS euuid,
456 practitioner.uuid AS pruuid
457 FROM procedure_order AS porder
458 LEFT JOIN procedure_order_code AS pcode
459 ON porder.procedure_order_id = pcode.procedure_order_id
460 LEFT JOIN procedure_report AS preport
461 ON preport.procedure_order_id = porder.procedure_order_id
462 LEFT JOIN procedure_result AS presult
463 ON presult.procedure_report_id = preport.procedure_report_id
464 LEFT JOIN patient_data AS patient
465 ON patient.pid = porder.patient_id
466 LEFT JOIN form_encounter AS encounter
467 ON encounter.encounter = porder.encounter_id
468 LEFT JOIN users AS practitioner
469 ON practitioner.id = porder.provider_id";
471 if (!empty($search)) {
472 $sql .= ' WHERE ';
473 if (!empty($puuidBind)) {
474 // code to support patient binding
475 $sql .= '(';
477 $whereClauses = array();
478 foreach ($search as $fieldName => $fieldValue) {
479 array_push($whereClauses, $fieldName . ' = ?');
480 array_push($sqlBindArray, $fieldValue);
482 $sqlCondition = ($isAndCondition == true) ? 'AND' : 'OR';
483 $sql .= implode(' ' . $sqlCondition . ' ', $whereClauses);
484 if (!empty($puuidBind)) {
485 // code to support patient binding
486 $sql .= ") AND `patient`.`uuid` = ?";
487 $sqlBindArray[] = UuidRegistry::uuidToBytes($puuidBind);
489 } elseif (!empty($puuidBind)) {
490 // code to support patient binding
491 $sql .= " WHERE `patient`.`uuid` = ?";
492 $sqlBindArray[] = UuidRegistry::uuidToBytes($puuidBind);
495 $statementResults = sqlStatement($sql, $sqlBindArray);
497 $processingResult = new ProcessingResult();
498 while ($row = sqlFetchArray($statementResults)) {
499 $row['uuid'] = UuidRegistry::uuidToString($row['uuid']);
500 $row['puuid'] = UuidRegistry::uuidToString($row['puuid']);
501 $row['euuid'] = UuidRegistry::uuidToString($row['euuid']);
502 $row['pruuid'] = UuidRegistry::uuidToString($row['pruuid']);
503 if ($row['order_diagnosis'] != "") {
504 $row['order_diagnosis'] = $this->addDiagnosis($row['order_diagnosis']);
506 if ($row['diagnoses'] != "") {
507 $row['diagnoses'] = $this->addDiagnosis($row['diagnoses']);
509 $processingResult->addData($row);
512 return $processingResult;
516 * Returns a single procedure record by id.
517 * @param $uuid - The procedure uuid identifier in string format.
518 * @param $puuidBind - Optional variable to only allow visibility of the patient with this puuid.
519 * @return ProcessingResult which contains validation messages, internal error messages, and the data
520 * payload.
522 public function getOne($uuid, $puuidBind = null)
524 $processingResult = new ProcessingResult();
526 $isValid = BaseValidator::validateId("uuid", "procedure_order", $uuid, true);
527 if ($isValid !== true) {
528 $validationMessages = [
529 'uuid' => ["invalid or nonexisting value" => " value " . $uuid]
531 $processingResult->setValidationMessages($validationMessages);
532 return $processingResult;
535 if (!empty($puuidBind)) {
536 // code to support patient binding
537 $isValid = BaseValidator::validateId("uuid", self::PATIENT_TABLE, $puuidBind, true);
538 if ($isValid !== true) {
539 $validationMessages = [
540 'puuid' => ["invalid or nonexisting value" => " value " . $puuidBind]
542 $processingResult->setValidationMessages($validationMessages);
543 return $processingResult;
547 $sql = "SELECT porder.*,
548 pcode.diagnoses,
549 pcode.procedure_order_title,
550 pcode.procedure_name,
551 pcode.procedure_code,
552 presult.result_status,
553 presult.result_code,
554 presult.result_text,
555 presult.date,
556 presult.facility,
557 presult.units,
558 presult.result,
559 patient.uuid AS puuid,
560 encounter.uuid AS euuid,
561 practitioner.uuid AS pruuid
562 FROM procedure_order AS porder
563 LEFT JOIN procedure_order_code AS pcode
564 ON porder.procedure_order_id = pcode.procedure_order_id
565 LEFT JOIN procedure_report AS preport
566 ON preport.procedure_order_id = porder.procedure_order_id
567 LEFT JOIN procedure_result AS presult
568 ON presult.procedure_report_id = preport.procedure_report_id
569 LEFT JOIN patient_data AS patient
570 ON patient.pid = porder.patient_id
571 LEFT JOIN form_encounter AS encounter
572 ON encounter.encounter = porder.encounter_id
573 LEFT JOIN users AS practitioner
574 ON practitioner.id = porder.provider_id
575 WHERE porder.uuid = ?";
577 $uuidBinary = UuidRegistry::uuidToBytes($uuid);
578 $sqlBindArray = [$uuidBinary];
580 if (!empty($puuidBind)) {
581 // code to support patient binding
582 $sql .= " AND `patient`.`uuid` = ?";
583 $sqlBindArray[] = UuidRegistry::uuidToBytes($puuidBind);
586 $sqlResult = sqlQuery($sql, $sqlBindArray);
587 if (!empty($sqlResult)) {
588 $sqlResult['uuid'] = UuidRegistry::uuidToString($sqlResult['uuid']);
589 $sqlResult['puuid'] = UuidRegistry::uuidToString($sqlResult['puuid']);
590 $sqlResult['euuid'] = UuidRegistry::uuidToString($sqlResult['euuid']);
591 $sqlResult['pruuid'] = UuidRegistry::uuidToString($sqlResult['pruuid']);
592 if ($sqlResult['order_diagnosis'] != "") {
593 $sqlResult['order_diagnosis'] = $this->addDiagnosis($sqlResult['order_diagnosis']);
595 if ($sqlResult['diagnoses'] != "") {
596 $sqlResult['diagnoses'] = $this->addDiagnosis($sqlResult['diagnoses']);
598 $processingResult->addData($sqlResult);
600 return $processingResult;
603 public function addDiagnosis($data)
605 $diagnosisArray = array();
606 $dataArray = explode(";", $data);
607 foreach ($dataArray as $diagnosis) {
608 $diagnosisSplit = explode(":", $diagnosis);
609 array_push($diagnosisArray, $diagnosisSplit);
611 return $diagnosisArray;