Openemr fhir search (#4349)
[openemr.git] / src / Services / PatientService.php
bloba1b9669d9738411a17e7770008cb8ea850a31991
1 <?php
3 /**
4 * Patient Service
6 * @package OpenEMR
7 * @link http://www.open-emr.org
8 * @author Victor Kofia <victor.kofia@gmail.com>
9 * @author Brady Miller <brady.g.miller@gmail.com>
10 * @author Jerry Padgett <sjpadgett@gmail.com>
11 * @copyright Copyright (c) 2017 Victor Kofia <victor.kofia@gmail.com>
12 * @copyright Copyright (c) 2018 Brady Miller <brady.g.miller@gmail.com>
13 * @copyright Copyright (c) 2020 Jerry Padgett <sjpadgett@gmail.com>
14 * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3
17 namespace OpenEMR\Services;
19 use OpenEMR\Common\Uuid\UuidRegistry;
20 use OpenEMR\Events\Patient\BeforePatientCreatedEvent;
21 use OpenEMR\Events\Patient\BeforePatientUpdatedEvent;
22 use OpenEMR\Events\Patient\PatientCreatedEvent;
23 use OpenEMR\Events\Patient\PatientUpdatedEvent;
24 use OpenEMR\Services\Search\ISearchField;
25 use OpenEMR\Services\Search\TokenSearchField;
26 use OpenEMR\Services\Search\SearchModifier;
27 use OpenEMR\Services\Search\StringSearchField;
28 use OpenEMR\Services\Search\TokenSearchValue;
29 use OpenEMR\Validators\PatientValidator;
30 use OpenEMR\Validators\ProcessingResult;
32 class PatientService extends BaseService
34 const TABLE_NAME = 'patient_data';
36 /**
37 * In the case where a patient doesn't have a picture uploaded,
38 * this value will be returned so that the document controller
39 * can return an empty response.
41 private $patient_picture_fallback_id = -1;
43 private $patientValidator;
45 /**
46 * Default constructor.
48 public function __construct()
50 parent::__construct(self::TABLE_NAME);
51 $this->patientValidator = new PatientValidator();
54 /**
55 * TODO: This should go in the ChartTrackerService and doesn't have to be static.
57 * @param $pid unique patient id
58 * @return recordset
60 public static function getChartTrackerInformationActivity($pid)
62 $sql = "SELECT ct.ct_when,
63 ct.ct_userid,
64 ct.ct_location,
65 u.username,
66 u.fname,
67 u.mname,
68 u.lname
69 FROM chart_tracker AS ct
70 LEFT OUTER JOIN users AS u ON u.id = ct.ct_userid
71 WHERE ct.ct_pid = ?
72 ORDER BY ct.ct_when DESC";
73 return sqlStatement($sql, array($pid));
76 /**
77 * TODO: This should go in the ChartTrackerService and doesn't have to be static.
79 * @return recordset
81 public static function getChartTrackerInformation()
83 $sql = "SELECT ct.ct_when,
84 u.username,
85 u.fname AS ufname,
86 u.mname AS umname,
87 u.lname AS ulname,
88 p.pubpid,
89 p.fname,
90 p.mname,
91 p.lname
92 FROM chart_tracker AS ct
93 JOIN cttemp ON cttemp.ct_pid = ct.ct_pid AND cttemp.ct_when = ct.ct_when
94 LEFT OUTER JOIN users AS u ON u.id = ct.ct_userid
95 LEFT OUTER JOIN patient_data AS p ON p.pid = ct.ct_pid
96 WHERE ct.ct_userid != 0
97 ORDER BY p.pubpid";
98 return sqlStatement($sql);
101 public function getFreshPid()
103 $pid = sqlQuery("SELECT MAX(pid)+1 AS pid FROM patient_data");
104 return $pid['pid'] === null ? 1 : intval($pid['pid']);
108 * Insert a patient record into the database
110 * returns the newly-created patient data array, or false in the case of
111 * an error with the sql insert
113 * @param $data
114 * @return false|int
116 public function databaseInsert($data)
118 $freshPid = $this->getFreshPid();
119 $data['pid'] = $freshPid;
120 $data['uuid'] = (new UuidRegistry(['table_name' => 'patient_data']))->createUuid();
122 // The 'date' is the updated-date, and 'regdate' is the created-date
123 // so set both to the current datetime.
124 $data['date'] = date("Y-m-d H:i:s");
125 $data['regdate'] = date("Y-m-d H:i:s");
126 if (empty($data['pubpid'])) {
127 $data['pubpid'] = $freshPid;
130 // Before a patient is inserted, fire the "before patient created" event so listeners can do extra processing
131 $beforePatientCreatedEvent = new BeforePatientCreatedEvent($data);
132 $GLOBALS["kernel"]->getEventDispatcher()->dispatch(BeforePatientCreatedEvent::EVENT_HANDLE, $beforePatientCreatedEvent, 10);
133 $data = $beforePatientCreatedEvent->getPatientData();
135 $query = $this->buildInsertColumns($data);
136 $sql = " INSERT INTO patient_data SET ";
137 $sql .= $query['set'];
139 $results = sqlInsert(
140 $sql,
141 $query['bind']
144 // Tell subscribers that a new patient has been created
145 $patientCreatedEvent = new PatientCreatedEvent($data);
146 $GLOBALS["kernel"]->getEventDispatcher()->dispatch(PatientCreatedEvent::EVENT_HANDLE, $patientCreatedEvent, 10);
148 // If we have a result-set from our insert, return the PID,
149 // otherwise return false
150 if ($results) {
151 return $data;
152 } else {
153 return false;
158 * Inserts a new patient record.
160 * @param $data The patient fields (array) to insert.
161 * @return ProcessingResult which contains validation messages, internal error messages, and the data
162 * payload.
164 public function insert($data)
166 $processingResult = $this->patientValidator->validate($data, PatientValidator::DATABASE_INSERT_CONTEXT);
168 if (!$processingResult->isValid()) {
169 return $processingResult;
172 $data = $this->databaseInsert($data);
174 if (false !== $data['pid']) {
175 $processingResult->addData(array(
176 'pid' => $data['pid'],
177 'uuid' => UuidRegistry::uuidToString($data['uuid'])
179 } else {
180 $processingResult->addInternalError("error processing SQL Insert");
183 return $processingResult;
187 * Do a database update using the pid from the input
188 * array
190 * Return the data that was updated into the database,
191 * or false if there was an error with the update
193 * @param array $data
194 * @return mixed
196 public function databaseUpdate($data)
198 // Get the data before update to send to the event listener
199 $dataBeforeUpdate = $this->findByPid($data['pid']);
201 // The `date` column is treated as an updated_date
202 $data['date'] = date("Y-m-d H:i:s");
203 $table = PatientService::TABLE_NAME;
205 // Fire the "before patient updated" event so listeners can do extra processing before data is updated
206 $beforePatientUpdatedEvent = new BeforePatientUpdatedEvent($data);
207 $GLOBALS["kernel"]->getEventDispatcher()->dispatch(BeforePatientUpdatedEvent::EVENT_HANDLE, $beforePatientUpdatedEvent, 10);
208 $data = $beforePatientUpdatedEvent->getPatientData();
210 $query = $this->buildUpdateColumns($data);
211 $sql = " UPDATE $table SET ";
212 $sql .= $query['set'];
213 $sql .= " WHERE `pid` = ?";
215 array_push($query['bind'], $data['pid']);
216 $sqlResult = sqlStatement($sql, $query['bind']);
218 if ($sqlResult) {
219 // Tell subscribers that a new patient has been updated
220 $patientUpdatedEvent = new PatientUpdatedEvent($dataBeforeUpdate, $data);
221 $GLOBALS["kernel"]->getEventDispatcher()->dispatch(PatientUpdatedEvent::EVENT_HANDLE, $patientUpdatedEvent, 10);
223 return $data;
224 } else {
225 return false;
230 * Updates an existing patient record.
232 * @param $puuidString - The patient uuid identifier in string format used for update.
233 * @param $data - The updated patient data fields
234 * @return ProcessingResult which contains validation messages, internal error messages, and the data
235 * payload.
237 public function update($puuidString, $data)
239 $data["uuid"] = $puuidString;
240 $processingResult = $this->patientValidator->validate($data, PatientValidator::DATABASE_UPDATE_CONTEXT);
241 if (!$processingResult->isValid()) {
242 return $processingResult;
245 // Get the data before update to send to the event listener
246 $dataBeforeUpdate = $this->getOne($puuidString);
248 // The `date` column is treated as an updated_date
249 $data['date'] = date("Y-m-d H:i:s");
251 // Fire the "before patient updated" event so listeners can do extra processing before data is updated
252 $beforePatientUpdatedEvent = new BeforePatientUpdatedEvent($data);
253 $GLOBALS["kernel"]->getEventDispatcher()->dispatch(BeforePatientUpdatedEvent::EVENT_HANDLE, $beforePatientUpdatedEvent, 10);
254 $data = $beforePatientUpdatedEvent->getPatientData();
256 $query = $this->buildUpdateColumns($data);
257 $sql = " UPDATE patient_data SET ";
258 $sql .= $query['set'];
259 $sql .= " WHERE `uuid` = ?";
261 $puuidBinary = UuidRegistry::uuidToBytes($puuidString);
262 array_push($query['bind'], $puuidBinary);
263 $sqlResult = sqlStatement($sql, $query['bind']);
265 if (!$sqlResult) {
266 $processingResult->addErrorMessage("error processing SQL Update");
267 } else {
268 $processingResult = $this->getOne($puuidString);
269 // Tell subscribers that a new patient has been updated
270 // We have to do this here and in the databaseUpdate() because this lookup is
271 // by uuid where the databseUpdate updates by pid.
272 $patientUpdatedEvent = new PatientUpdatedEvent($dataBeforeUpdate, $processingResult->getData());
273 $GLOBALS["kernel"]->getEventDispatcher()->dispatch(PatientUpdatedEvent::EVENT_HANDLE, $patientUpdatedEvent, 10);
275 return $processingResult;
278 protected function createResultRecordFromDatabaseResult($record)
280 if (!empty($record['uuid'])) {
281 $record['uuid'] = UuidRegistry::uuidToString($record['uuid']);
284 return $record;
289 * Returns a list of patients matching optional search criteria.
290 * Search criteria is conveyed by array where key = field/column name, value = field value.
291 * If no search criteria is provided, all records are returned.
293 * @param $search search array parameters
294 * @param $isAndCondition specifies if AND condition is used for multiple criteria. Defaults to true.
295 * @param $puuidBind - Optional variable to only allow visibility of the patient with this puuid.
296 * @return ProcessingResult which contains validation messages, internal error messages, and the data
297 * payload.
299 public function getAll($search = array(), $isAndCondition = true, $puuidBind = null)
301 $querySearch = [];
302 if (!empty($search)) {
303 if (isset($puuidBind)) {
304 $querySearch['uuid'] = new TokenSearchField('uuid', $puuidBind);
305 } else if (isset($search['uuid'])) {
306 $querySearch['uuid'] = new TokenSearchField('uuid', $search['uuid']);
308 $wildcardFields = array('fname', 'mname', 'lname', 'street', 'city', 'state','postal_code','title');
309 foreach ($wildcardFields as $field) {
310 if (isset($search[$field])) {
311 $querySearch[$field] = new StringSearchField($field, $search[$field], SearchModifier::CONTAINS, $isAndCondition);
315 return $this->search($querySearch, $isAndCondition);
319 * Returns a single patient record by patient id.
320 * @param $puuidString - The patient uuid identifier in string format.
321 * @return ProcessingResult which contains validation messages, internal error messages, and the data
322 * payload.
324 public function getOne($puuidString)
326 $processingResult = new ProcessingResult();
328 $isValid = $this->patientValidator->isExistingUuid($puuidString);
330 if (!$isValid) {
331 $validationMessages = [
332 'uuid' => ["invalid or nonexisting value" => " value " . $puuidString]
334 $processingResult->setValidationMessages($validationMessages);
335 return $processingResult;
338 $sql = "SELECT id,
339 pid,
340 uuid,
341 pubpid,
342 title,
343 fname,
344 mname,
345 lname,
347 street,
348 postal_code,
349 city,
350 state,
351 county,
352 country_code,
353 drivers_license,
354 contact_relationship,
355 phone_contact,
356 phone_home,
357 phone_biz,
358 phone_cell,
359 email,
360 DOB,
361 sex,
362 race,
363 ethnicity,
364 status,
365 `language`
366 FROM patient_data
367 WHERE uuid = ?";
369 $puuidBinary = UuidRegistry::uuidToBytes($puuidString);
370 $sqlResult = sqlQuery($sql, [$puuidBinary]);
372 $sqlResult['uuid'] = UuidRegistry::uuidToString($sqlResult['uuid']);
373 $processingResult->addData($sqlResult);
374 return $processingResult;
378 * Given a pid, find the patient record
380 * @param $pid
382 public function findByPid($pid)
384 $table = PatientService::TABLE_NAME;
385 $patientRow = self::selectHelper("SELECT * FROM `$table`", [
386 'where' => 'WHERE pid = ?',
387 'limit' => 1,
388 'data' => [$pid]
391 return $patientRow;
395 * @return number
397 public function getPatientPictureDocumentId($pid)
399 $sql = "SELECT doc.id AS id
400 FROM documents doc
401 JOIN categories_to_documents cate_to_doc
402 ON doc.id = cate_to_doc.document_id
403 JOIN categories cate
404 ON cate.id = cate_to_doc.category_id
405 WHERE cate.name LIKE ? and doc.foreign_id = ?";
407 $result = sqlQuery($sql, array($GLOBALS['patient_photo_category_name'], $pid));
409 if (empty($result) || empty($result['id'])) {
410 return $this->patient_picture_fallback_id;
413 return $result['id'];
417 * Fetch UUID for the patient id
419 * @param string $id - ID of Patient
420 * @return false if nothing found otherwise return UUID
422 public function getUuid($pid)
424 return self::getUuidById($pid, self::TABLE_NAME, 'pid');