<?php
namespace MedBrief\MSR\Entity;
use DH\Auditor\Provider\Doctrine\Auditing\Annotation as Audit;
use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
use MedBrief\MSR\Repository\StudyRepository;
/**
* @ORM\Table(name="Study", indexes={@ORM\Index(name="orthanc_study_id_index", columns={"orthanc_study_id"})})
*
* @ORM\Entity(repositoryClass=StudyRepository::class)
*
* @Gedmo\SoftDeleteable(fieldName="deletedAt", timeAware=false)
*
* @Audit\Auditable
*
* @Audit\Security(view={"ROLE_ALLOWED_TO_AUDIT"})
*/
class Study
{
public const ORTHANC_SYNC_STATUS_PENDING = 1;
public const ORTHANC_SYNC_STATUS_FAILED = 2;
public const ORTHANC_SYNC_STATUS_SUCCESS = 3;
/**
* @var int
*
* @ORM\Column(name="id", type="integer")
*
* @ORM\Id
*
* @ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* @var \DateTime|null
*
* @ORM\Column(name="deletedAt", type="datetime", nullable=true)
*/
private $deletedAt;
/**
* @var string|null
*
* @ORM\Column(name="dicom_study_description", type="string", nullable=true)
*/
private $dicom_study_description;
/**
* @var string|null
*
* @ORM\Column(name="dicom_study_id", type="string", nullable=true)
*/
private $dicom_study_id;
/**
* @var int|null
*
* @ORM\Column(name="orthanc_sync_status", type="integer", nullable=true)
*/
private $orthanc_sync_status;
/**
* @var string|null
*
* @ORM\Column(name="orthanc_study_id", type="string", nullable=true)
*/
private $orthanc_study_id;
/**
* @var string|null
*
* @ORM\Column(name="orthanc_parent_patient_id", type="string", length=255, nullable=true)
*/
private $orthanc_parent_patient_id;
/**
* @var string|null
*
* @ORM\Column(type="string", length=255, nullable=true)
*/
private $studyDescription;
/**
* @var \DateTimeImmutable|null
*
* @ORM\Column(type="datetime_immutable", nullable=true)
*/
private $studyDate;
/**
* @var string|null
*
* @ORM\Column(type="string", nullable=true, length=255)
*/
private $studyInstituteName;
/**
* @var \DateTime
*
* @ORM\Column(name="created", type="datetime")
*
* @Gedmo\Timestampable(on="create")
*/
private $created;
/**
* @var \DateTime
*
* @ORM\Column(name="updated", type="datetime")
*
* @Gedmo\Timestampable(on="update")
*/
private $updated;
/**
* @var \Doctrine\Common\Collections\Collection
*
* @ORM\OneToMany(targetEntity="MedBrief\MSR\Entity\Series", mappedBy="study")
*/
private $series;
/**
* @var \Doctrine\Common\Collections\Collection
*
* @ORM\OneToMany(targetEntity="MedBrief\MSR\Entity\OrthancTransaction", mappedBy="study", cascade={"all"})
*/
private $orthancTransactions;
/**
* @var Patient
*
* @ORM\ManyToOne(targetEntity="MedBrief\MSR\Entity\Patient", inversedBy="studies", cascade={"persist"})
*
* @ORM\JoinColumns({
*
* @ORM\JoinColumn(name="patient_id", referencedColumnName="id", nullable=true)
* })
*/
private $patient;
/**
* @var Project
*
* @ORM\ManyToOne(targetEntity="MedBrief\MSR\Entity\Project", inversedBy="studies")
*
* @ORM\JoinColumns({
*
* @ORM\JoinColumn(name="project_id", referencedColumnName="id", nullable=false)
* })
*/
private $project;
/**
* @var \Doctrine\Common\Collections\Collection
*
* @ORM\ManyToMany(targetEntity="MedBrief\MSR\Entity\Disc", inversedBy="studies")
*
* @ORM\JoinTable(name="study_disc",
* joinColumns={
*
* @ORM\JoinColumn(name="study_id", referencedColumnName="id", onDelete="CASCADE")
* },
* inverseJoinColumns={
* @ORM\JoinColumn(name="disc_id", referencedColumnName="id", onDelete="CASCADE")
* }
* )
*/
private $discs;
/**
* Constructor
*/
public function __construct()
{
$this->series = new \Doctrine\Common\Collections\ArrayCollection();
$this->orthancTransactions = new \Doctrine\Common\Collections\ArrayCollection();
$this->discs = new \Doctrine\Common\Collections\ArrayCollection();
}
public function __clone()
{
if ($this->id) {
$this->id = null;
$this->series = new \Doctrine\Common\Collections\ArrayCollection();
$this->orthancTransactions = new \Doctrine\Common\Collections\ArrayCollection();
$this->discs = new \Doctrine\Common\Collections\ArrayCollection();
}
}
/**
* Returns the Patient name and study description
*
* @return string
*/
public function __toString()
{
if ($date = $this->getStudyDate()) {
$return = $date->format('Y-m-d');
} else {
$return = 'Unknown Date';
}
$return .= ' - ' . $this->getStudyDescription();
// If we recorded the Discs from which this study originated, then include it in the name
if ($this->hasDiscs()) {
if ($this->getDiscs()->count() === 1) {
return $return . ' - ' . $this->getDiscs()->first()->getName();
}
$discNames = '(';
/** @var Disc $disc */
foreach ($this->getDiscs() as $disc) {
$discNames .= sprintf('%s, ', $disc->getName());
}
$discNames = rtrim($discNames, ', ');
$discNames .= ')';
return $return . ' - ' . $discNames;
}
return $return;
}
/**
* Get id
*
* @return int
*/
public function getId()
{
return $this->id;
}
/**
* Set dicom_study_name
*
* @param string $dicomStudyName
*
* @return Study
*/
public function setDicomStudyName($dicomStudyName)
{
// TODO: @Deon investigate why this property is missing
$this->dicom_study_name = $dicomStudyName;
return $this;
}
/**
* Get dicom_study_name
*
* @return string
*/
public function getDicomStudyName()
{
return $this->dicom_study_name;
}
/**
* Set dicom_study_id
*
* @param string $dicomStudyId
*
* @return Study
*/
public function setDicomStudyId($dicomStudyId)
{
$this->dicom_study_id = $dicomStudyId;
return $this;
}
/**
* Get dicom_study_id
*
* @return string
*/
public function getDicomStudyId()
{
return $this->dicom_study_id;
}
/**
* Set created
*
* @param \DateTime $created
*
* @return Study
*/
public function setCreated($created)
{
$this->created = $created;
return $this;
}
/**
* Get created
*
* @return \DateTime
*/
public function getCreated()
{
return $this->created;
}
/**
* Set updated
*
* @param \DateTime $updated
*
* @return Study
*/
public function setUpdated($updated)
{
$this->updated = $updated;
return $this;
}
/**
* Get updated
*
* @return \DateTime
*/
public function getUpdated()
{
return $this->updated;
}
/**
* Add series
*
* @param Series $series
*
* @return Study
*/
public function addSeries(Series $series)
{
$this->series[] = $series;
return $this;
}
/**
* Remove series
*
* @param Series $series
*/
public function removeSeries(Series $series)
{
$this->series->removeElement($series);
}
/**
* Get series
*
* @return \Doctrine\Common\Collections\Collection|Series[]
*/
public function getSeries()
{
return $this->series;
}
/**
* Set patient
*
* @param Patient $patient
*
* @return Study
*/
public function setPatient(Patient $patient)
{
$this->patient = $patient;
return $this;
}
/**
* Get patient
*
* @return Patient
*/
public function getPatient()
{
return $this->patient;
}
/**
* Set project
*
* @param Project $project
*
* @return Study
*/
public function setProject(?Project $project)
{
$this->project = $project;
return $this;
}
/**
* Get project
*
* @return Project
*/
public function getProject()
{
return $this->project;
}
/**
* Set dicom_study_description
*
* @param string $dicomStudyDescription
*
* @return Study
*/
public function setDicomStudyDescription($dicomStudyDescription)
{
$this->dicom_study_description = $dicomStudyDescription;
return $this;
}
/**
* Get dicom_study_description
*
* @return string
*/
public function getDicomStudyDescription()
{
return $this->dicom_study_description;
}
/**
* Set orthancStudyId
*
* @param string $orthancStudyId
*
* @return Study
*/
public function setOrthancStudyId($orthancStudyId)
{
$this->orthanc_study_id = $orthancStudyId;
return $this;
}
/**
* Get orthancStudyId
*
* @return string
*/
public function getOrthancStudyId()
{
return $this->orthanc_study_id;
}
/**
* Add orthancTransaction
*
* @param OrthancTransaction $orthancTransaction
*
* @throws \Exception
*
* @return Study
*/
public function addOrthancTransaction(OrthancTransaction $orthancTransaction)
{
$this->orthancTransactions[] = $orthancTransaction;
// based on the type of the transaction, we run the appropriate function
// on this Study
switch ($orthancTransaction->getType()) {
case OrthancTransaction::TYPE_STUDY_DETAILS:
$this->processOrthancStudyDetailsTransaction($orthancTransaction);
break;
default:
throw new \Exception('Invalid OrthancTransaction Type Assigned to Study: ' . $orthancTransaction->getType());
}
return $this;
}
/**
* Given the orthancTransaction (which is assumed to be of type STUDY DETAILS)
* We update the appropriate values on this Study to indicate the various
* orthanc data returned by the storage transaction
*
* @param OrthancTransaction $orthancTransaction
*
* @return void | false
*/
public function processOrthancStudyDetailsTransaction(OrthancTransaction $orthancTransaction)
{
// first - set the submissions status to pending at the outset
$this->setOrthancSyncStatus(self::ORTHANC_SYNC_STATUS_PENDING);
// if the given transaction wasn't a success, the storage status of this
// document is automatically also a fail
if ($orthancTransaction->getTransactionStatus() != OrthancTransaction::TRANSACTION_STATUS_SUCCESS) {
$this->setOrthancSyncStatus(self::ORTHANC_SYNC_STATUS_FAILED);
return false;
}
$jsonObject = json_decode($orthancTransaction->getTransactionResponse());
$this->setOrthancParentPatientId($jsonObject->ParentPatient);
// set the status as successful
$this->setOrthancSyncStatus(self::ORTHANC_SYNC_STATUS_SUCCESS);
}
/**
* Remove orthancTransaction
*
* @param OrthancTransaction $orthancTransaction
*/
public function removeOrthancTransaction(OrthancTransaction $orthancTransaction)
{
$this->orthancTransactions->removeElement($orthancTransaction);
}
/**
* Get orthancTransactions
*
* @return \Doctrine\Common\Collections\Collection
*/
public function getOrthancTransactions()
{
return $this->orthancTransactions;
}
/**
* Set orthancSyncStatus
*
* @param int $orthancSyncStatus
*
* @return Study
*/
public function setOrthancSyncStatus($orthancSyncStatus)
{
$this->orthanc_sync_status = $orthancSyncStatus;
return $this;
}
/**
* Get orthancSyncStatus
*
* @return int
*/
public function getOrthancSyncStatus()
{
return $this->orthanc_sync_status;
}
/**
* Set orthancParentPatientId
*
* @param string $orthancParentPatientId
*
* @return Study
*/
public function setOrthancParentPatientId($orthancParentPatientId)
{
$this->orthanc_parent_patient_id = $orthancParentPatientId;
return $this;
}
/**
* Get orthancParentPatientId
*
* @return string
*/
public function getOrthancParentPatientId()
{
return $this->orthanc_parent_patient_id;
}
/**
* Returns a Associative Array which is a json_decoded array from the latest Orthanc
* Transaction that performed a details request for this Study
*
* @return null | string
*/
public function getOrthancDetails()
{
@trigger_error(sprintf('Function %s is deprecated; please set details directly on the entity.', __FUNCTION__), E_USER_DEPRECATED);
// get the latest successful Study Details Transaction
$latestOrthancStudyDetailsTransaction = $this->getLatestOrthancStudyDetailsTransaction();
// if there isn't one, then we return null
if (!$latestOrthancStudyDetailsTransaction) {
return null;
}
// otherwise we return the json decoded response
$transactionResponse = $latestOrthancStudyDetailsTransaction->getTransactionResponse();
return json_decode($transactionResponse, true);
}
/**
* Returns the most recent Orthanc Study Details Transaction linked to this
* Study. If there is one, then it will contain all the details about
* this Study that are currently stored in Orthanc
*
* @return null
*/
public function getLatestOrthancStudyDetailsTransaction()
{
// create some criteria that will find successful transactions of the
// appropriate type, ordered latest first
$criteria = Criteria::create()
->andWhere(Criteria::expr()->eq('transaction_status', OrthancTransaction::TRANSACTION_STATUS_SUCCESS))
->andWhere(Criteria::expr()->eq('type', OrthancTransaction::TYPE_STUDY_DETAILS))
->orderBy(['created' => Criteria::DESC])
;
// apply the criteria
$matchingTransactions = $this->getOrthancTransactions()->matching($criteria);
// if there are not matches, then return null
if ($matchingTransactions->isEmpty()) {
return null;
}
// else return the first match
$firstMatchingTransaction = $matchingTransactions->first();
return $firstMatchingTransaction;
}
public function getOrthancDicomDate()
{
// first prize is that this Study has already been synced to Orthanc
// and the details have already been retrieved for this Study
$orthancDetails = $this->getOrthancDetails();
// if not then we need to at least return something
if (!$orthancDetails) {
return 'Unknown Date';
}
// otherwise we assume that our Orthanc Details are structured correctly
// and we return the Study Description
$studyDate = isset($orthancDetails['MainDicomTags']['StudyDate']) ? date('Y-m-d', strtotime($orthancDetails['MainDicomTags']['StudyDate'])) : 'Unknown Date';
return $studyDate;
}
public function getOrthancDicomDescription()
{
// first prize is that this Study has already been synced to Orthanc
// and the details have already been retrieved for this Study
$orthancDetails = $this->getOrthancDetails();
// if not then we need to at least return something
if (!$orthancDetails) {
return 'Unknown Study';
}
// otherwise we assume that our Orthanc Details are structured correctly
// and we return the Study Description
// note the leading ampersands which suppress notices, which we can't
// have in the __toString method;
$studyDescription = @$orthancDetails['MainDicomTags']['StudyDescription'] ?: @$orthancDetails['MainDicomTags']['StudyID'];
if (empty($studyDescription)) {
$studyDescription = 'No Study Description';
}
return $studyDescription;
}
/**
* Set deletedAt
*
* @param \DateTime $deletedAt
*
* @return Study
*/
public function setDeletedAt($deletedAt)
{
$this->deletedAt = $deletedAt;
return $this;
}
/**
* Get deletedAt
*
* @return \DateTime
*/
public function getDeletedAt()
{
return $this->deletedAt;
}
/**
* Add series
*
* @param Disc $disc
*
* @return Study
*/
public function addDisc(Disc $disc)
{
$this->discs[] = $disc;
return $this;
}
/**
* Remove series
*
* @param Disc $disc
*/
public function removeDisc(Disc $disc)
{
$this->discs->removeElement($disc);
}
/**
* Get series
*
* @return \Doctrine\Common\Collections\Collection
*/
public function getDiscs()
{
return $this->discs;
}
/**
* Check to see whether this study has any Discs
*
* @return bool
*/
public function hasDiscs()
{
return !$this->discs->isEmpty();
}
/**
* Adds a Disc to the Study
*
* @deprecated Use of this function is deprecated as the relationship between Studies and Discs has changed.
* Please update code to use ::addDisc($disc) instead.
*
* @param Disc $disc
*
* @return Study
*/
public function setDisc(Disc $disc)
{
@trigger_error(sprintf('Use of the %s function is deprecated. Please update accordingly.', __FUNCTION__), E_USER_DEPRECATED);
return $this->addDisc($disc);
}
/**
* Gets a Disc associated to a Study
*
* @deprecated Use of this function is deprecated as the relationship between Studies and Discs has changed.
* Please update code to use ::getDiscs() instead and adapt accordingly.
*
* @return Disc|false
*/
public function getDisc()
{
@trigger_error(sprintf('Use of the %s function is deprecated. Please update accordingly.', __FUNCTION__), E_USER_DEPRECATED);
// For safety's sake so we don't break existing code, let's return the first disc.
return $this->getDiscs()->first();
}
/**
* Returns the persisted StudyDescription where available, falling back to the legacy call.
*
* @return string
*/
public function getStudyDescription(): string
{
return $this->studyDescription ? $this->studyDescription : $this->getOrthancDicomDescription();
}
/**
* @param string $studyDescription
*
* @return Study
*/
public function setStudyDescription(string $studyDescription): Study
{
$this->studyDescription = $studyDescription;
return $this;
}
/**
* This function will either return the stored StudyDate from Orthanc or,
* failing that, null, indicating we have no saved StudyDate.
*
* @return \DateTimeImmutable|null
*/
public function getStudyDate(): ?\DateTimeImmutable
{
// First prize: Native study date stored
if ($this->studyDate !== null) {
return $this->studyDate;
}
// Second Prize: From the legacy date
try {
return new \DateTimeImmutable($this->getOrthancDicomDate());
} catch (\Exception $exception) {
// Booby prize: Unknown Date
return null;
}
}
/**
* @param \DateTimeImmutable $studyDate
*
* @return Study
*/
public function setStudyDate(\DateTimeImmutable $studyDate): Study
{
$this->studyDate = $studyDate;
return $this;
}
/**
* Returns the Study's associated InstituteName DICOM value when persisted, falling back to the
* legacy Orthanc details when necessary.
*
* @return string
*/
public function getStudyInstituteName(): string
{
if ($this->studyInstituteName !== null) {
return $this->studyInstituteName;
}
$studyDetails = $this->getOrthancDetails();
return $studyDetails['MainDicomTags']['InstitutionName'] ?? 'Not Specified';
}
/**
* @param string|null $studyInstituteName
*
* @return Study
*/
public function setStudyInstituteName(?string $studyInstituteName): Study
{
$this->studyInstituteName = $studyInstituteName;
return $this;
}
}