src/Entity/Study.php line 22

Open in your IDE?
  1. <?php
  2. namespace MedBrief\MSR\Entity;
  3. use DH\Auditor\Provider\Doctrine\Auditing\Annotation as Audit;
  4. use Doctrine\Common\Collections\Criteria;
  5. use Doctrine\ORM\Mapping as ORM;
  6. use Gedmo\Mapping\Annotation as Gedmo;
  7. use MedBrief\MSR\Repository\StudyRepository;
  8. /**
  9. * @ORM\Table(name="Study", indexes={@ORM\Index(name="orthanc_study_id_index", columns={"orthanc_study_id"})})
  10. *
  11. * @ORM\Entity(repositoryClass=StudyRepository::class)
  12. *
  13. * @Gedmo\SoftDeleteable(fieldName="deletedAt", timeAware=false)
  14. *
  15. * @Audit\Auditable
  16. *
  17. * @Audit\Security(view={"ROLE_ALLOWED_TO_AUDIT"})
  18. */
  19. class Study
  20. {
  21. public const ORTHANC_SYNC_STATUS_PENDING = 1;
  22. public const ORTHANC_SYNC_STATUS_FAILED = 2;
  23. public const ORTHANC_SYNC_STATUS_SUCCESS = 3;
  24. /**
  25. * @var int
  26. *
  27. * @ORM\Column(name="id", type="integer")
  28. *
  29. * @ORM\Id
  30. *
  31. * @ORM\GeneratedValue(strategy="IDENTITY")
  32. */
  33. private $id;
  34. /**
  35. * @var \DateTime|null
  36. *
  37. * @ORM\Column(name="deletedAt", type="datetime", nullable=true)
  38. */
  39. private $deletedAt;
  40. /**
  41. * @var string|null
  42. *
  43. * @ORM\Column(name="dicom_study_description", type="string", nullable=true)
  44. */
  45. private $dicom_study_description;
  46. /**
  47. * @var string|null
  48. *
  49. * @ORM\Column(name="dicom_study_id", type="string", nullable=true)
  50. */
  51. private $dicom_study_id;
  52. /**
  53. * @var int|null
  54. *
  55. * @ORM\Column(name="orthanc_sync_status", type="integer", nullable=true)
  56. */
  57. private $orthanc_sync_status;
  58. /**
  59. * @var string|null
  60. *
  61. * @ORM\Column(name="orthanc_study_id", type="string", nullable=true)
  62. */
  63. private $orthanc_study_id;
  64. /**
  65. * @var string|null
  66. *
  67. * @ORM\Column(name="orthanc_parent_patient_id", type="string", length=255, nullable=true)
  68. */
  69. private $orthanc_parent_patient_id;
  70. /**
  71. * @var string|null
  72. *
  73. * @ORM\Column(type="string", length=255, nullable=true)
  74. */
  75. private $studyDescription;
  76. /**
  77. * @var \DateTimeImmutable|null
  78. *
  79. * @ORM\Column(type="datetime_immutable", nullable=true)
  80. */
  81. private $studyDate;
  82. /**
  83. * @var string|null
  84. *
  85. * @ORM\Column(type="string", nullable=true, length=255)
  86. */
  87. private $studyInstituteName;
  88. /**
  89. * @var \DateTime
  90. *
  91. * @ORM\Column(name="created", type="datetime")
  92. *
  93. * @Gedmo\Timestampable(on="create")
  94. */
  95. private $created;
  96. /**
  97. * @var \DateTime
  98. *
  99. * @ORM\Column(name="updated", type="datetime")
  100. *
  101. * @Gedmo\Timestampable(on="update")
  102. */
  103. private $updated;
  104. /**
  105. * @var \Doctrine\Common\Collections\Collection
  106. *
  107. * @ORM\OneToMany(targetEntity="MedBrief\MSR\Entity\Series", mappedBy="study")
  108. */
  109. private $series;
  110. /**
  111. * @var \Doctrine\Common\Collections\Collection
  112. *
  113. * @ORM\OneToMany(targetEntity="MedBrief\MSR\Entity\OrthancTransaction", mappedBy="study", cascade={"all"})
  114. */
  115. private $orthancTransactions;
  116. /**
  117. * @var Patient
  118. *
  119. * @ORM\ManyToOne(targetEntity="MedBrief\MSR\Entity\Patient", inversedBy="studies", cascade={"persist"})
  120. *
  121. * @ORM\JoinColumns({
  122. *
  123. * @ORM\JoinColumn(name="patient_id", referencedColumnName="id", nullable=true)
  124. * })
  125. */
  126. private $patient;
  127. /**
  128. * @var Project
  129. *
  130. * @ORM\ManyToOne(targetEntity="MedBrief\MSR\Entity\Project", inversedBy="studies")
  131. *
  132. * @ORM\JoinColumns({
  133. *
  134. * @ORM\JoinColumn(name="project_id", referencedColumnName="id", nullable=false)
  135. * })
  136. */
  137. private $project;
  138. /**
  139. * @var \Doctrine\Common\Collections\Collection
  140. *
  141. * @ORM\ManyToMany(targetEntity="MedBrief\MSR\Entity\Disc", inversedBy="studies")
  142. *
  143. * @ORM\JoinTable(name="study_disc",
  144. * joinColumns={
  145. *
  146. * @ORM\JoinColumn(name="study_id", referencedColumnName="id", onDelete="CASCADE")
  147. * },
  148. * inverseJoinColumns={
  149. * @ORM\JoinColumn(name="disc_id", referencedColumnName="id", onDelete="CASCADE")
  150. * }
  151. * )
  152. */
  153. private $discs;
  154. /**
  155. * Constructor
  156. */
  157. public function __construct()
  158. {
  159. $this->series = new \Doctrine\Common\Collections\ArrayCollection();
  160. $this->orthancTransactions = new \Doctrine\Common\Collections\ArrayCollection();
  161. $this->discs = new \Doctrine\Common\Collections\ArrayCollection();
  162. }
  163. public function __clone()
  164. {
  165. if ($this->id) {
  166. $this->id = null;
  167. $this->series = new \Doctrine\Common\Collections\ArrayCollection();
  168. $this->orthancTransactions = new \Doctrine\Common\Collections\ArrayCollection();
  169. $this->discs = new \Doctrine\Common\Collections\ArrayCollection();
  170. }
  171. }
  172. /**
  173. * Returns the Patient name and study description
  174. *
  175. * @return string
  176. */
  177. public function __toString()
  178. {
  179. if ($date = $this->getStudyDate()) {
  180. $return = $date->format('Y-m-d');
  181. } else {
  182. $return = 'Unknown Date';
  183. }
  184. $return .= ' - ' . $this->getStudyDescription();
  185. // If we recorded the Discs from which this study originated, then include it in the name
  186. if ($this->hasDiscs()) {
  187. if ($this->getDiscs()->count() === 1) {
  188. return $return . ' - ' . $this->getDiscs()->first()->getName();
  189. }
  190. $discNames = '(';
  191. /** @var Disc $disc */
  192. foreach ($this->getDiscs() as $disc) {
  193. $discNames .= sprintf('%s, ', $disc->getName());
  194. }
  195. $discNames = rtrim($discNames, ', ');
  196. $discNames .= ')';
  197. return $return . ' - ' . $discNames;
  198. }
  199. return $return;
  200. }
  201. /**
  202. * Get id
  203. *
  204. * @return int
  205. */
  206. public function getId()
  207. {
  208. return $this->id;
  209. }
  210. /**
  211. * Set dicom_study_name
  212. *
  213. * @param string $dicomStudyName
  214. *
  215. * @return Study
  216. */
  217. public function setDicomStudyName($dicomStudyName)
  218. {
  219. // TODO: @Deon investigate why this property is missing
  220. $this->dicom_study_name = $dicomStudyName;
  221. return $this;
  222. }
  223. /**
  224. * Get dicom_study_name
  225. *
  226. * @return string
  227. */
  228. public function getDicomStudyName()
  229. {
  230. return $this->dicom_study_name;
  231. }
  232. /**
  233. * Set dicom_study_id
  234. *
  235. * @param string $dicomStudyId
  236. *
  237. * @return Study
  238. */
  239. public function setDicomStudyId($dicomStudyId)
  240. {
  241. $this->dicom_study_id = $dicomStudyId;
  242. return $this;
  243. }
  244. /**
  245. * Get dicom_study_id
  246. *
  247. * @return string
  248. */
  249. public function getDicomStudyId()
  250. {
  251. return $this->dicom_study_id;
  252. }
  253. /**
  254. * Set created
  255. *
  256. * @param \DateTime $created
  257. *
  258. * @return Study
  259. */
  260. public function setCreated($created)
  261. {
  262. $this->created = $created;
  263. return $this;
  264. }
  265. /**
  266. * Get created
  267. *
  268. * @return \DateTime
  269. */
  270. public function getCreated()
  271. {
  272. return $this->created;
  273. }
  274. /**
  275. * Set updated
  276. *
  277. * @param \DateTime $updated
  278. *
  279. * @return Study
  280. */
  281. public function setUpdated($updated)
  282. {
  283. $this->updated = $updated;
  284. return $this;
  285. }
  286. /**
  287. * Get updated
  288. *
  289. * @return \DateTime
  290. */
  291. public function getUpdated()
  292. {
  293. return $this->updated;
  294. }
  295. /**
  296. * Add series
  297. *
  298. * @param Series $series
  299. *
  300. * @return Study
  301. */
  302. public function addSeries(Series $series)
  303. {
  304. $this->series[] = $series;
  305. return $this;
  306. }
  307. /**
  308. * Remove series
  309. *
  310. * @param Series $series
  311. */
  312. public function removeSeries(Series $series)
  313. {
  314. $this->series->removeElement($series);
  315. }
  316. /**
  317. * Get series
  318. *
  319. * @return \Doctrine\Common\Collections\Collection|Series[]
  320. */
  321. public function getSeries()
  322. {
  323. return $this->series;
  324. }
  325. /**
  326. * Set patient
  327. *
  328. * @param Patient $patient
  329. *
  330. * @return Study
  331. */
  332. public function setPatient(Patient $patient)
  333. {
  334. $this->patient = $patient;
  335. return $this;
  336. }
  337. /**
  338. * Get patient
  339. *
  340. * @return Patient
  341. */
  342. public function getPatient()
  343. {
  344. return $this->patient;
  345. }
  346. /**
  347. * Set project
  348. *
  349. * @param Project $project
  350. *
  351. * @return Study
  352. */
  353. public function setProject(?Project $project)
  354. {
  355. $this->project = $project;
  356. return $this;
  357. }
  358. /**
  359. * Get project
  360. *
  361. * @return Project
  362. */
  363. public function getProject()
  364. {
  365. return $this->project;
  366. }
  367. /**
  368. * Set dicom_study_description
  369. *
  370. * @param string $dicomStudyDescription
  371. *
  372. * @return Study
  373. */
  374. public function setDicomStudyDescription($dicomStudyDescription)
  375. {
  376. $this->dicom_study_description = $dicomStudyDescription;
  377. return $this;
  378. }
  379. /**
  380. * Get dicom_study_description
  381. *
  382. * @return string
  383. */
  384. public function getDicomStudyDescription()
  385. {
  386. return $this->dicom_study_description;
  387. }
  388. /**
  389. * Set orthancStudyId
  390. *
  391. * @param string $orthancStudyId
  392. *
  393. * @return Study
  394. */
  395. public function setOrthancStudyId($orthancStudyId)
  396. {
  397. $this->orthanc_study_id = $orthancStudyId;
  398. return $this;
  399. }
  400. /**
  401. * Get orthancStudyId
  402. *
  403. * @return string
  404. */
  405. public function getOrthancStudyId()
  406. {
  407. return $this->orthanc_study_id;
  408. }
  409. /**
  410. * Add orthancTransaction
  411. *
  412. * @param OrthancTransaction $orthancTransaction
  413. *
  414. * @throws \Exception
  415. *
  416. * @return Study
  417. */
  418. public function addOrthancTransaction(OrthancTransaction $orthancTransaction)
  419. {
  420. $this->orthancTransactions[] = $orthancTransaction;
  421. // based on the type of the transaction, we run the appropriate function
  422. // on this Study
  423. switch ($orthancTransaction->getType()) {
  424. case OrthancTransaction::TYPE_STUDY_DETAILS:
  425. $this->processOrthancStudyDetailsTransaction($orthancTransaction);
  426. break;
  427. default:
  428. throw new \Exception('Invalid OrthancTransaction Type Assigned to Study: ' . $orthancTransaction->getType());
  429. }
  430. return $this;
  431. }
  432. /**
  433. * Given the orthancTransaction (which is assumed to be of type STUDY DETAILS)
  434. * We update the appropriate values on this Study to indicate the various
  435. * orthanc data returned by the storage transaction
  436. *
  437. * @param OrthancTransaction $orthancTransaction
  438. *
  439. * @return void | false
  440. */
  441. public function processOrthancStudyDetailsTransaction(OrthancTransaction $orthancTransaction)
  442. {
  443. // first - set the submissions status to pending at the outset
  444. $this->setOrthancSyncStatus(self::ORTHANC_SYNC_STATUS_PENDING);
  445. // if the given transaction wasn't a success, the storage status of this
  446. // document is automatically also a fail
  447. if ($orthancTransaction->getTransactionStatus() != OrthancTransaction::TRANSACTION_STATUS_SUCCESS) {
  448. $this->setOrthancSyncStatus(self::ORTHANC_SYNC_STATUS_FAILED);
  449. return false;
  450. }
  451. $jsonObject = json_decode($orthancTransaction->getTransactionResponse());
  452. $this->setOrthancParentPatientId($jsonObject->ParentPatient);
  453. // set the status as successful
  454. $this->setOrthancSyncStatus(self::ORTHANC_SYNC_STATUS_SUCCESS);
  455. }
  456. /**
  457. * Remove orthancTransaction
  458. *
  459. * @param OrthancTransaction $orthancTransaction
  460. */
  461. public function removeOrthancTransaction(OrthancTransaction $orthancTransaction)
  462. {
  463. $this->orthancTransactions->removeElement($orthancTransaction);
  464. }
  465. /**
  466. * Get orthancTransactions
  467. *
  468. * @return \Doctrine\Common\Collections\Collection
  469. */
  470. public function getOrthancTransactions()
  471. {
  472. return $this->orthancTransactions;
  473. }
  474. /**
  475. * Set orthancSyncStatus
  476. *
  477. * @param int $orthancSyncStatus
  478. *
  479. * @return Study
  480. */
  481. public function setOrthancSyncStatus($orthancSyncStatus)
  482. {
  483. $this->orthanc_sync_status = $orthancSyncStatus;
  484. return $this;
  485. }
  486. /**
  487. * Get orthancSyncStatus
  488. *
  489. * @return int
  490. */
  491. public function getOrthancSyncStatus()
  492. {
  493. return $this->orthanc_sync_status;
  494. }
  495. /**
  496. * Set orthancParentPatientId
  497. *
  498. * @param string $orthancParentPatientId
  499. *
  500. * @return Study
  501. */
  502. public function setOrthancParentPatientId($orthancParentPatientId)
  503. {
  504. $this->orthanc_parent_patient_id = $orthancParentPatientId;
  505. return $this;
  506. }
  507. /**
  508. * Get orthancParentPatientId
  509. *
  510. * @return string
  511. */
  512. public function getOrthancParentPatientId()
  513. {
  514. return $this->orthanc_parent_patient_id;
  515. }
  516. /**
  517. * Returns a Associative Array which is a json_decoded array from the latest Orthanc
  518. * Transaction that performed a details request for this Study
  519. *
  520. * @return null | string
  521. */
  522. public function getOrthancDetails()
  523. {
  524. @trigger_error(sprintf('Function %s is deprecated; please set details directly on the entity.', __FUNCTION__), E_USER_DEPRECATED);
  525. // get the latest successful Study Details Transaction
  526. $latestOrthancStudyDetailsTransaction = $this->getLatestOrthancStudyDetailsTransaction();
  527. // if there isn't one, then we return null
  528. if (!$latestOrthancStudyDetailsTransaction) {
  529. return null;
  530. }
  531. // otherwise we return the json decoded response
  532. $transactionResponse = $latestOrthancStudyDetailsTransaction->getTransactionResponse();
  533. return json_decode($transactionResponse, true);
  534. }
  535. /**
  536. * Returns the most recent Orthanc Study Details Transaction linked to this
  537. * Study. If there is one, then it will contain all the details about
  538. * this Study that are currently stored in Orthanc
  539. *
  540. * @return null
  541. */
  542. public function getLatestOrthancStudyDetailsTransaction()
  543. {
  544. // create some criteria that will find successful transactions of the
  545. // appropriate type, ordered latest first
  546. $criteria = Criteria::create()
  547. ->andWhere(Criteria::expr()->eq('transaction_status', OrthancTransaction::TRANSACTION_STATUS_SUCCESS))
  548. ->andWhere(Criteria::expr()->eq('type', OrthancTransaction::TYPE_STUDY_DETAILS))
  549. ->orderBy(['created' => Criteria::DESC])
  550. ;
  551. // apply the criteria
  552. $matchingTransactions = $this->getOrthancTransactions()->matching($criteria);
  553. // if there are not matches, then return null
  554. if ($matchingTransactions->isEmpty()) {
  555. return null;
  556. }
  557. // else return the first match
  558. $firstMatchingTransaction = $matchingTransactions->first();
  559. return $firstMatchingTransaction;
  560. }
  561. public function getOrthancDicomDate()
  562. {
  563. // first prize is that this Study has already been synced to Orthanc
  564. // and the details have already been retrieved for this Study
  565. $orthancDetails = $this->getOrthancDetails();
  566. // if not then we need to at least return something
  567. if (!$orthancDetails) {
  568. return 'Unknown Date';
  569. }
  570. // otherwise we assume that our Orthanc Details are structured correctly
  571. // and we return the Study Description
  572. $studyDate = isset($orthancDetails['MainDicomTags']['StudyDate']) ? date('Y-m-d', strtotime($orthancDetails['MainDicomTags']['StudyDate'])) : 'Unknown Date';
  573. return $studyDate;
  574. }
  575. public function getOrthancDicomDescription()
  576. {
  577. // first prize is that this Study has already been synced to Orthanc
  578. // and the details have already been retrieved for this Study
  579. $orthancDetails = $this->getOrthancDetails();
  580. // if not then we need to at least return something
  581. if (!$orthancDetails) {
  582. return 'Unknown Study';
  583. }
  584. // otherwise we assume that our Orthanc Details are structured correctly
  585. // and we return the Study Description
  586. // note the leading ampersands which suppress notices, which we can't
  587. // have in the __toString method;
  588. $studyDescription = @$orthancDetails['MainDicomTags']['StudyDescription'] ?: @$orthancDetails['MainDicomTags']['StudyID'];
  589. if (empty($studyDescription)) {
  590. $studyDescription = 'No Study Description';
  591. }
  592. return $studyDescription;
  593. }
  594. /**
  595. * Set deletedAt
  596. *
  597. * @param \DateTime $deletedAt
  598. *
  599. * @return Study
  600. */
  601. public function setDeletedAt($deletedAt)
  602. {
  603. $this->deletedAt = $deletedAt;
  604. return $this;
  605. }
  606. /**
  607. * Get deletedAt
  608. *
  609. * @return \DateTime
  610. */
  611. public function getDeletedAt()
  612. {
  613. return $this->deletedAt;
  614. }
  615. /**
  616. * Add series
  617. *
  618. * @param Disc $disc
  619. *
  620. * @return Study
  621. */
  622. public function addDisc(Disc $disc)
  623. {
  624. $this->discs[] = $disc;
  625. return $this;
  626. }
  627. /**
  628. * Remove series
  629. *
  630. * @param Disc $disc
  631. */
  632. public function removeDisc(Disc $disc)
  633. {
  634. $this->discs->removeElement($disc);
  635. }
  636. /**
  637. * Get series
  638. *
  639. * @return \Doctrine\Common\Collections\Collection
  640. */
  641. public function getDiscs()
  642. {
  643. return $this->discs;
  644. }
  645. /**
  646. * Check to see whether this study has any Discs
  647. *
  648. * @return bool
  649. */
  650. public function hasDiscs()
  651. {
  652. return !$this->discs->isEmpty();
  653. }
  654. /**
  655. * Adds a Disc to the Study
  656. *
  657. * @deprecated Use of this function is deprecated as the relationship between Studies and Discs has changed.
  658. * Please update code to use ::addDisc($disc) instead.
  659. *
  660. * @param Disc $disc
  661. *
  662. * @return Study
  663. */
  664. public function setDisc(Disc $disc)
  665. {
  666. @trigger_error(sprintf('Use of the %s function is deprecated. Please update accordingly.', __FUNCTION__), E_USER_DEPRECATED);
  667. return $this->addDisc($disc);
  668. }
  669. /**
  670. * Gets a Disc associated to a Study
  671. *
  672. * @deprecated Use of this function is deprecated as the relationship between Studies and Discs has changed.
  673. * Please update code to use ::getDiscs() instead and adapt accordingly.
  674. *
  675. * @return Disc|false
  676. */
  677. public function getDisc()
  678. {
  679. @trigger_error(sprintf('Use of the %s function is deprecated. Please update accordingly.', __FUNCTION__), E_USER_DEPRECATED);
  680. // For safety's sake so we don't break existing code, let's return the first disc.
  681. return $this->getDiscs()->first();
  682. }
  683. /**
  684. * Returns the persisted StudyDescription where available, falling back to the legacy call.
  685. *
  686. * @return string
  687. */
  688. public function getStudyDescription(): string
  689. {
  690. return $this->studyDescription ? $this->studyDescription : $this->getOrthancDicomDescription();
  691. }
  692. /**
  693. * @param string $studyDescription
  694. *
  695. * @return Study
  696. */
  697. public function setStudyDescription(string $studyDescription): Study
  698. {
  699. $this->studyDescription = $studyDescription;
  700. return $this;
  701. }
  702. /**
  703. * This function will either return the stored StudyDate from Orthanc or,
  704. * failing that, null, indicating we have no saved StudyDate.
  705. *
  706. * @return \DateTimeImmutable|null
  707. */
  708. public function getStudyDate(): ?\DateTimeImmutable
  709. {
  710. // First prize: Native study date stored
  711. if ($this->studyDate !== null) {
  712. return $this->studyDate;
  713. }
  714. // Second Prize: From the legacy date
  715. try {
  716. return new \DateTimeImmutable($this->getOrthancDicomDate());
  717. } catch (\Exception $exception) {
  718. // Booby prize: Unknown Date
  719. return null;
  720. }
  721. }
  722. /**
  723. * @param \DateTimeImmutable $studyDate
  724. *
  725. * @return Study
  726. */
  727. public function setStudyDate(\DateTimeImmutable $studyDate): Study
  728. {
  729. $this->studyDate = $studyDate;
  730. return $this;
  731. }
  732. /**
  733. * Returns the Study's associated InstituteName DICOM value when persisted, falling back to the
  734. * legacy Orthanc details when necessary.
  735. *
  736. * @return string
  737. */
  738. public function getStudyInstituteName(): string
  739. {
  740. if ($this->studyInstituteName !== null) {
  741. return $this->studyInstituteName;
  742. }
  743. $studyDetails = $this->getOrthancDetails();
  744. return $studyDetails['MainDicomTags']['InstitutionName'] ?? 'Not Specified';
  745. }
  746. /**
  747. * @param string|null $studyInstituteName
  748. *
  749. * @return Study
  750. */
  751. public function setStudyInstituteName(?string $studyInstituteName): Study
  752. {
  753. $this->studyInstituteName = $studyInstituteName;
  754. return $this;
  755. }
  756. }