src/Service/MasterListPdf.php line 10

Open in your IDE?
  1. <?php
  2. namespace App\Service;
  3. use Mpdf\Mpdf;
  4. use Mpdf\Output\Destination;
  5. use Symfony\Component\HttpFoundation\Response;
  6. class MasterListPdf
  7. {
  8. // Column widths (mm) for the registrations table — sums to ~178mm inside
  9. // the 180mm content area (matches the original layout's proportions).
  10. private const COL_WIDTHS = [8, 40, 65, 23, 26, 8, 8];
  11. private const COL_HEADERS = ['Seat', 'Student', 'Exam', 'JSE ID', 'Contact #', 'Dur', 'ID?'];
  12. private const COL_MAXLEN = [4, 20, 33, 14, 14, 14, 4];
  13. /**
  14. * Generates the master list.
  15. *
  16. * - If $mpdf is supplied, a page is appended to that existing document
  17. * (used by CoverSheetService when building one combined session PDF)
  18. * and this method returns null.
  19. * - If $mpdf is null, a standalone document is created and either its
  20. * bytes are returned, or (when called directly from the browser) a
  21. * Symfony Response with the PDF attached is returned.
  22. */
  23. public static function generate($masterListData = null, $testSessionsService, $studentsRepository, $vendorsRepository, ?Mpdf $mpdf = null)
  24. {
  25. $toBrowser = false;
  26. if ($masterListData === null) {
  27. $toBrowser = true;
  28. $testSessionId = $_REQUEST['sessionId'];
  29. $masterListData = $testSessionsService->getMasterListData($testSessionId);
  30. }
  31. $rawName = $masterListData['testSessionName'];
  32. // Convert date format: "Israel - 2026-05-03 - 4:00PM - Israel by Appointment"
  33. // to: "Israel - Sun May 3, 2026 - 4:00PM - Israel by Appointment"
  34. $testSessionDescriptor = preg_replace_callback(
  35. '/(\d{4}-\d{2}-\d{2})/',
  36. function ($matches) {
  37. $date = new \DateTime($matches[1]);
  38. return $date->format('D M j, Y');
  39. },
  40. $rawName
  41. );
  42. $ownDoc = ($mpdf === null);
  43. if ($ownDoc) {
  44. $mpdf = self::makeMpdf();
  45. }
  46. $mpdf->SetTitle('JSE Session Master List - ' . $testSessionDescriptor);
  47. $mpdf->SetSubject('JSE Session Master List Subject - ' . $testSessionDescriptor);
  48. $mpdf->SetKeywords('jsematerials, jse, jewish subject exams');
  49. $mpdf->SetCreator('JSE-IL-version');
  50. $mpdf->SetAuthor('Yaffie Goldberg');
  51. $logoPath = realpath(__DIR__ . '/../../assets/images/jseLogo.png');
  52. $mpdf->SetHTMLHeader(self::masterListHeaderHtml($logoPath, $testSessionDescriptor));
  53. $mpdf->AddPageByArray([
  54. 'orientation' => 'P',
  55. 'margin-left' => 15,
  56. 'margin-right' => 15,
  57. 'margin-top' => 35,
  58. 'margin-bottom' => 25,
  59. 'margin-header' => 3,
  60. 'margin-footer' => 10,
  61. 'resetpagenum' => 1,
  62. ]);
  63. $mpdf->SetHTMLFooter(
  64. '<div style="text-align:center; font-family:Arial; font-size:8pt; border-top:0.2mm solid #999999; padding-top:1mm;">'
  65. . 'Page {PAGENO} of {nbpg}'
  66. . '</div>'
  67. );
  68. $totalStudents = $masterListData['totalStudents'];
  69. $html = '<div style="font-family:Arial; font-size:10pt; margin-top:3mm; margin-bottom:3mm;">'
  70. . 'Total Students registered for test session: <b>' . htmlspecialchars((string) $totalStudents) . '</b>'
  71. . '</div>';
  72. foreach ($masterListData['sections'] as $section) {
  73. $sectionName = htmlspecialchars((string) $section['sectionName']);
  74. $html .= '<div style="font-family:Arial; font-size:10pt; font-weight:bold; margin-top:2mm; margin-bottom:1mm;">Section ' . $sectionName . '</div>';
  75. $rows = [];
  76. foreach ($section['registrations'] as $registration) {
  77. $seat = $registration['seat'] ?? 'X';
  78. $student = $registration['student'];
  79. $fullStudent = $studentsRepository->find($student['id']);
  80. $studentFirstName = $fullStudent->getFirstName();
  81. $studentLastName = $fullStudent->getLastName();
  82. $studentName = "$studentLastName, $studentFirstName";
  83. $cd_required = $registration['exam']['cd_required'];
  84. $vendorName = $vendorsRepository->getVendorName($registration['exam']['vendor']);
  85. $examSubject = $vendorName . " - " . $registration['exam']['name'];
  86. $rows[] = [
  87. (string) $seat,
  88. (string) $studentName,
  89. (string) $examSubject,
  90. (string) ($fullStudent->getJseId() ?? ''),
  91. (string) ($fullStudent->getPhone() ?? ''),
  92. (string) ($registration['exam']['duration'] ?? ''),
  93. ];
  94. if ($fullStudent->getAccommodations()) {
  95. $rows[] = [
  96. '', '', '',
  97. (string) $fullStudent->getAccommodations(),
  98. (string) ($fullStudent->getAccommodationsApproved() ? "Approved" : "(not approved)"),
  99. '',
  100. ];
  101. }
  102. if ($cd_required == 1) {
  103. $rows[] = ['', '', 'THIS EXAM NEEDS A CD','', '', ''];
  104. }
  105. }
  106. $html .= self::sectionTableHtml($rows);
  107. }
  108. $mpdf->WriteHTML($html);
  109. if (!$ownDoc) {
  110. // Page(s) appended to the shared document; caller continues.
  111. return null;
  112. }
  113. $ret = $mpdf->Output('', Destination::STRING_RETURN);
  114. if ($toBrowser) {
  115. $testSessionName = $testSessionsService->generateTestSessionName($testSessionId, true);
  116. $response = new Response($ret);
  117. $response->headers->set('Content-Type', 'application/pdf');
  118. $response->headers->set('Content-Disposition', 'attachment; filename="' . $testSessionName . '.pdf"');
  119. $response->headers->set('Content-Length', (string) strlen($ret));
  120. return $response;
  121. }
  122. return $ret;
  123. }
  124. /**
  125. * Shared mPDF configuration for a standalone master list document.
  126. */
  127. private static function makeMpdf(): Mpdf
  128. {
  129. return new Mpdf([
  130. 'mode' => 'utf-8',
  131. 'format' => 'A4',
  132. 'margin_left' => 15,
  133. 'margin_right' => 15,
  134. 'margin_top' => 35,
  135. 'margin_bottom' => 25,
  136. 'margin_header' => 3,
  137. 'margin_footer' => 0,
  138. 'default_font' => 'arial',
  139. ]);
  140. }
  141. private static function masterListHeaderHtml(string $logoPath, string $testSessionDescriptor): string
  142. {
  143. return '
  144. <table style="width:180mm;">
  145. <tr>
  146. <td style="width:32mm; vertical-align:top;">
  147. <img src="' . htmlspecialchars($logoPath) . '" style="width:30mm; height:25mm;" />
  148. </td>
  149. <td style="vertical-align:top; font-family:Arial;">
  150. <div style="font-weight:bold; font-size:10pt;">JSE Master List</div>
  151. <div style="font-size:10pt; margin-top:2mm;">' . htmlspecialchars($testSessionDescriptor) . '</div>
  152. </td>
  153. </tr>
  154. </table>
  155. <div style="border-bottom:0.3mm solid #000000; margin-top:1mm; line-height:0; font-size:0;">&#160;</div>
  156. ';
  157. }
  158. /**
  159. * Builds the registrations table for one section. Uses a native HTML
  160. * <thead>, so mPDF automatically repeats the column header row on every
  161. * page break within the table — replacing the manual page-break /
  162. * re-draw-header logic from the tc-lib-pdf version.
  163. */
  164. private static function sectionTableHtml(array $rows): string
  165. {
  166. $html = '<table style="width:178mm; border-collapse:collapse; font-family:Arial; font-size:10pt;" cellpadding="2">';
  167. $html .= '<thead><tr>';
  168. foreach (self::COL_HEADERS as $i => $label) {
  169. $html .= '<td style="width:' . self::COL_WIDTHS[$i] . 'mm; background-color:#d0d0ff; border:0.3mm solid #0000ff; font-weight:bold; text-align:center; padding:2mm 1mm;">'
  170. . htmlspecialchars($label) . '</td>';
  171. }
  172. $html .= '</tr></thead><tbody>';
  173. $fill = false;
  174. foreach ($rows as $row) {
  175. $bg = $fill ? '#e0ebff' : '#ffffff';
  176. $html .= '<tr>';
  177. for ($i = 0; $i < 7; $i++) {
  178. $cell = $i < 6 ? mb_substr((string) ($row[$i] ?? ''), 0, self::COL_MAXLEN[$i]) : '';
  179. $html .= '<td style="width:' . self::COL_WIDTHS[$i] . 'mm; background-color:' . $bg . '; border:0.3mm solid #0000ff; text-align:left; padding:1.5mm 1mm;">'
  180. . htmlspecialchars($cell) . '</td>';
  181. }
  182. $html .= '</tr>';
  183. $fill = !$fill;
  184. }
  185. $html .= '</tbody></table>';
  186. return $html;
  187. }
  188. }