@@ -237,7 +237,12 @@ public function deleteCertificate(): bool
237237 /**
238238 * Generates (or updates) the user's certificate as a Resource.
239239 *
240- * - Stores the HTML in a ResourceNode (resource_type = user_certificate or fallback handled at repository level).
240+ * Template strategy is centralized here:
241+ * 1) If the Gradebook category HAS a default template (document) => use it.
242+ * 2) If it DOES NOT => fall back to the portal "custom" template (legacy behavior).
243+ * In both cases we store a category-bound Resource (cat_id), so pages don't need to re-implement fallbacks.
244+ *
245+ * - Stores the HTML in a ResourceNode (resource_type = user_certificate).
241246 * - Fills $this->certificate_data['file_content'] with the HTML to avoid PDF errors.
242247 * - Keeps legacy DB info via registerUserInfoAboutCertificate() (no PersonalFile usage).
243248 *
@@ -254,7 +259,17 @@ public function generate($params = [], $sendNotification = false)
254259 ? (bool ) $ params ['hide_print_button ' ]
255260 : false ;
256261
257- $ certRepo = Container::getGradeBookCertificateRepository ();
262+ // Repository (required).
263+ try {
264+ $ certRepo = Container::getGradeBookCertificateRepository ();
265+ } catch (\Throwable $ e ) {
266+ error_log ('[CERT::generate] FATAL: cannot get GradeBookCertificateRepository: ' .$ e ->getMessage ());
267+ return false ;
268+ }
269+ if (!$ certRepo ) {
270+ error_log ('[CERT::generate] FATAL: GradeBookCertificateRepository is NULL ' );
271+ return false ;
272+ }
258273
259274 $ categoryId = 0 ;
260275 $ category = null ;
@@ -267,33 +282,57 @@ public function generate($params = [], $sendNotification = false)
267282 // Category::load() returns an array
268283 $ myCategory = Category::load ($ categoryId );
269284
270- $ repo = Container::getGradeBookCategoryRepository ();
271- /** @var \Chamilo\CoreBundle\Entity\GradebookCategory|null $category */
272- $ category = $ repo ->find ($ categoryId );
285+ try {
286+ $ repo = Container::getGradeBookCategoryRepository ();
287+ /** @var GradebookCategory|null $category */
288+ $ category = $ repo ? $ repo ->find ($ categoryId ) : null ;
289+ } catch (\Throwable $ e ) {
290+ error_log ('[CERT::generate] category repo fetch failed: ' .$ e ->getMessage ());
291+ $ category = null ;
292+ }
273293
274294 if (!empty ($ categoryId ) && !empty ($ myCategory ) && isset ($ myCategory [0 ])) {
275295 $ isCertificateAvailableInCategory = $ myCategory [0 ]->is_certificate_available ($ this ->user_id );
276296 }
277297 }
278298
279- // Path A: course/session-bound certificate
299+ // Path A: course/session-bound certificate (category context)
280300 if ($ isCertificateAvailableInCategory && null !== $ category ) {
281301 // Course/session info
282302 $ course = $ category ->getCourse ();
283303 $ courseInfo = api_get_course_info ($ course ->getCode ());
284304 $ courseId = $ courseInfo ['real_id ' ];
285305 $ sessionId = $ category ->getSession () ? (int ) $ category ->getSession ()->getId () : 0 ;
286306
287- // Award related skill
288- $ skill = new SkillModel ();
289- $ skill ->addSkillToUser (
290- $ this ->user_id ,
291- $ category ,
292- $ courseId ,
293- $ sessionId
294- );
307+ try {
308+ $ skill = new SkillModel ();
309+ $ skill ->addSkillToUser (
310+ $ this ->user_id ,
311+ $ category ,
312+ $ courseId ,
313+ $ sessionId
314+ );
315+ } catch (\Throwable $ e ) {
316+ error_log ('[CERT::generate] addSkillToUser failed: ' .$ e ->getMessage ());
317+ }
318+
319+ $ categoryHasDefaultTemplate = false ;
320+ $ documentIdForLog = null ;
321+ try {
322+ $ doc = $ category ->getDocument ();
323+ if ($ doc !== null ) {
324+ $ categoryHasDefaultTemplate = true ;
325+ try {
326+ $ documentIdForLog = method_exists ($ doc , 'getId ' ) ? $ doc ->getId () : null ;
327+ } catch (\Throwable $ ignored ) {
328+ $ documentIdForLog = null ;
329+ }
330+ }
331+ } catch (\Throwable $ e ) {
332+ error_log ('[CERT::generate] getDocument() failed (no default template): ' .$ e ->getMessage ());
333+ $ categoryHasDefaultTemplate = false ;
334+ }
295335
296- // Build certificate HTML and score
297336 $ gb = GradebookUtils::get_user_certificate_content (
298337 $ this ->user_id ,
299338 $ course ->getId (),
@@ -302,19 +341,41 @@ public function generate($params = [], $sendNotification = false)
302341 $ params ['hide_print_button ' ]
303342 );
304343
305- $ html = '' ;
306344 $ score = 100.0 ;
345+ if (is_array ($ gb ) && isset ($ gb ['score ' ])) {
346+ $ score = (float )$ gb ['score ' ];
347+ }
348+
349+ $ html = '' ;
350+ $ source = '' ;
307351
308- if (is_array ($ gb )) {
309- $ html = isset ($ gb ['content ' ]) ? (string ) $ gb ['content ' ] : '' ;
310- $ score = isset ($ gb ['score ' ]) ? (float ) $ gb ['score ' ] : 100.0 ;
311- } elseif (is_string ($ gb ) && $ gb !== '' ) {
312- // Some custom implementations might return a raw string
313- $ html = $ gb ;
352+ if ($ categoryHasDefaultTemplate ) {
353+ if (is_array ($ gb ) && !empty ($ gb ['content ' ])) {
354+ $ html = (string )$ gb ['content ' ];
355+ $ source = 'DEFAULT_TEMPLATE ' ;
356+ } elseif (is_string ($ gb ) && $ gb !== '' ) {
357+ $ html = $ gb ;
358+ $ source = 'DEFAULT_TEMPLATE ' ;
359+ }
360+ } else {
361+ error_log (sprintf (
362+ '[CERT::generate] course DEFAULT template NOT found. cat=%d user=%d -> fallback to CUSTOM ' ,
363+ (int )$ categoryId ,
364+ (int )$ this ->user_id
365+ ));
366+ }
367+
368+ if ($ html === '' ) {
369+ $ html = $ this ->generateCustomCertificate ('' );
370+ $ source = 'CUSTOM_TEMPLATE_FALLBACK ' ;
314371 }
315372
316373 if ($ html === '' ) {
317- error_log ('[CERT::generate] Empty HTML content for category certificate (cat= ' .$ categoryId .', user= ' .$ this ->user_id .'). ' );
374+ error_log (sprintf (
375+ '[CERT::generate] Empty HTML on category path. cat=%d user=%d ' ,
376+ (int )$ categoryId ,
377+ (int )$ this ->user_id
378+ ));
318379 return false ;
319380 }
320381
@@ -324,14 +385,19 @@ public function generate($params = [], $sendNotification = false)
324385 $ certRepo ->registerUserInfoAboutCertificate ($ categoryId , $ this ->user_id , $ score );
325386
326387 // Ensure PDF flow has the HTML in memory
327- $ this ->certificate_data ['file_content ' ] = $ html ;
328- $ this ->certificate_data ['path_certificate ' ] = '' ; // stored as resource, no legacy file path
388+ $ this ->certificate_data ['file_content ' ] = $ html ;
389+ $ this ->certificate_data ['path_certificate ' ] = '' ;
329390
330391 // Send notification if required (we have course context here)
331392 if ($ sendNotification ) {
332393 $ subject = get_lang ('Certificate notification ' );
333394 $ message = nl2br (get_lang ('((user_first_name)), ' ));
334- $ htmlUrl = $ certRepo ->getResourceFileUrl ($ entity );
395+ $ htmlUrl = '' ;
396+ try {
397+ $ htmlUrl = $ certRepo ->getResourceFileUrl ($ entity );
398+ } catch (\Throwable $ e ) {
399+ error_log ('[CERT::generate] getResourceFileUrl failed for notification: ' .$ e ->getMessage ());
400+ }
335401
336402 self ::sendNotification (
337403 $ subject ,
@@ -347,32 +413,42 @@ public function generate($params = [], $sendNotification = false)
347413
348414 return true ;
349415 } catch (\Throwable $ e ) {
350- error_log ('[CERT::generate] Upsert failed for category certificate (cat= ' .$ categoryId .', user= ' .$ this ->user_id .'): ' .$ e ->getMessage ());
416+ error_log (sprintf (
417+ '[CERT::generate] Upsert FAILED (course). cat=%d user=%d err=%s ' ,
418+ (int )$ categoryId ,
419+ (int )$ this ->user_id ,
420+ $ e ->getMessage ()
421+ ));
351422 return false ;
352423 }
353424 }
354425
355426 // Path B: general (portal-wide) certificate
356427 try {
357- $ html = $ this ->generateCustomCertificate ('' );
358- $ score = 100.0 ;
428+ $ html = $ this ->generateCustomCertificate ('' );
429+ $ score = 100.0 ;
359430
360431 if ($ html === '' ) {
361- error_log ('[CERT::generate] Empty HTML content for general certificate (user= ' .$ this ->user_id .'). ' );
432+ error_log (sprintf (
433+ '[CERT::generate] Empty HTML on general path. user=%d ' ,
434+ (int )$ this ->user_id
435+ ));
362436 return false ;
363437 }
364438
365439 $ entity = $ certRepo ->upsertCertificateResource (0 , $ this ->user_id , $ score , $ html );
366440 $ certRepo ->registerUserInfoAboutCertificate (0 , $ this ->user_id , $ score );
367441
368- // Ensure PDF flow has the HTML in memory
369442 $ this ->certificate_data ['file_content ' ] = $ html ;
370- $ this ->certificate_data ['path_certificate ' ] = '' ; // stored as resource
443+ $ this ->certificate_data ['path_certificate ' ] = '' ; // resource
371444
372- // No course context here, so we skip notification (sendNotification would fail its own checks)
373445 return true ;
374446 } catch (\Throwable $ e ) {
375- error_log ('[CERT::generate] General certificate upsert failed (user= ' .$ this ->user_id .'): ' .$ e ->getMessage ());
447+ error_log (sprintf (
448+ '[CERT::generate] Upsert FAILED (general). user=%d err=%s ' ,
449+ (int )$ this ->user_id ,
450+ $ e ->getMessage ()
451+ ));
376452 return false ;
377453 }
378454 }
@@ -876,6 +952,7 @@ public function generatePdfFromCustomCertificate(): void
876952 $ page_format = 'landscape ' == $ params ['orientation ' ] ? 'A4-L ' : 'A4 ' ;
877953 $ pdf = new PDF ($ page_format , $ params ['orientation ' ], $ params );
878954
955+ // Safety: ensure HTML content is present; fetch from Resource if needed.
879956 if (empty ($ this ->certificate_data ['file_content ' ])) {
880957 try {
881958 $ certRepo = Container::getGradeBookCertificateRepository ();
0 commit comments