Source: includes/internal/quiz-submission/submission/repositories/class-comments-based-submission-repository.php

<?php
/**
 * File containing the Comments_Based_Submission_Repository class.
 *
 * @package sensei
 */

namespace Sensei\Internal\Quiz_Submission\Submission\Repositories;

use DateTimeImmutable;
use DateTimeInterface;
use RuntimeException;
use Sensei\Internal\Quiz_Submission\Submission\Models\Comments_Based_Submission;
use Sensei\Internal\Quiz_Submission\Submission\Models\Submission_Interface;
use Sensei_Utils;
use WP_Comment;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Class Comments_Based_Submission_Repository.
 *
 * @internal
 *
 * @since 4.7.2
 */
class Comments_Based_Submission_Repository implements Submission_Repository_Interface {
	/**
	 * Creates a new quiz submission.
	 *
	 * @internal
	 *
	 * @param int        $quiz_id     The quiz ID.
	 * @param int        $user_id     The user ID.
	 * @param float|null $final_grade The final grade.
	 *
	 * @return Submission_Interface The quiz submission.
	 * @throws RuntimeException     In case the lesson status is missing.
	 */
	public function create( int $quiz_id, int $user_id, float $final_grade = null ): Submission_Interface {
		/**
		 * Filters the quiz ID when quiz submission is created.
		 *
		 * @hook sensei_quiz_submission_create_quiz_id
		 *
		 * @since 4.23.1
		 *
		 * @param {int} $quiz_id The quiz ID.
		 * @return {int} The quiz ID.
		*/
		$quiz_id = apply_filters( 'sensei_quiz_submission_create_quiz_id', $quiz_id );

		$status_comment = $this->get_status_comment( $quiz_id, $user_id );

		if ( ! $status_comment ) {
			throw new RuntimeException( 'Missing lesson status.' );
		}

		if ( null !== $final_grade ) {
			update_comment_meta( $status_comment->comment_ID, 'grade', $final_grade );
		}

		$created_at = $this->get_created_date( $status_comment );

		return new Comments_Based_Submission(
			$status_comment->comment_ID,
			$quiz_id,
			$user_id,
			$final_grade,
			$created_at,
			$created_at
		);
	}

	/**
	 * Get or create a new quiz submission if it doesn't exist.
	 *
	 * @internal
	 *
	 * @param int        $quiz_id     The quiz ID.
	 * @param int        $user_id     The user ID.
	 * @param float|null $final_grade The final grade.
	 *
	 * @return Submission_Interface The quiz submission.
	 */
	public function get_or_create( int $quiz_id, int $user_id, float $final_grade = null ): Submission_Interface {
		/**
		 * Filters the quiz ID when quiz submission is created.
		 *
		 * @hook sensei_quiz_submission_get_or_create_quiz_id
		 *
		 * @since 4.23.1
		 *
		 * @param {int} $quiz_id The quiz ID.
		 * @return {int} The quiz ID.
		 */
		$quiz_id = apply_filters( 'sensei_quiz_submission_get_or_create_quiz_id', $quiz_id );

		$submission = $this->get( $quiz_id, $user_id );

		if ( $submission ) {
			return $submission;
		}

		return $this->create( $quiz_id, $user_id, $final_grade );
	}

	/**
	 * Gets a quiz submission.
	 *
	 * @internal
	 *
	 * @param int $quiz_id The quiz ID.
	 * @param int $user_id The user ID.
	 *
	 * @return Submission_Interface|null The quiz submission.
	 */
	public function get( int $quiz_id, int $user_id ): ?Submission_Interface {
		/**
		 * Filters the quiz ID when quiz submission is retrieved.
		 *
		 * @hook sensei_quiz_submission_get_quiz_id
		 *
		 * @since 4.23.1
		 *
		 * @param {int} $quiz_id The quiz ID.
		 * @return {int} The quiz ID.
		 */
		$quiz_id = apply_filters( 'sensei_quiz_submission_get_quiz_id', $quiz_id );

		$status_comment = $this->get_status_comment( $quiz_id, $user_id );

		if (
			! $status_comment
			|| ! $this->get_question_ids( $status_comment->comment_ID )
		) {
			return null;
		}

		$final_grade = get_comment_meta( $status_comment->comment_ID, 'grade', true );
		$created_at  = $this->get_created_date( $status_comment );

		return new Comments_Based_Submission(
			$status_comment->comment_ID,
			$quiz_id,
			$user_id,
			is_numeric( $final_grade ) ? $final_grade : null,
			$created_at,
			$created_at
		);
	}

	/**
	 * Get the questions related to the quiz submission.
	 *
	 * @internal
	 *
	 * @param int $submission_id The quiz submission ID.
	 *
	 * @return array An array of question post IDs.
	 */
	public function get_question_ids( int $submission_id ): array {
		/**
		 * Filters the quiz submission ID when getting the question IDs.
		 *
		 * @hook sensei_quiz_submission_get_question_ids_submission_id
		 *
		 * @since 4.23.1
		 *
		 * @param {int}    $submission_id The quiz submission ID.
		 * @param {string} $context       The context.
		 * @return {int} The quiz submission ID.
		 */
		$submission_id = apply_filters( 'sensei_quiz_submission_get_question_ids_submission_id', $submission_id, 'comments' );

		$questions_asked_csv = get_comment_meta( $submission_id, 'questions_asked', true );
		if ( ! $questions_asked_csv ) {
			return [];
		}

		return array_map(
			'intval',
			explode( ',', $questions_asked_csv )
		);
	}

	/**
	 * Save quiz submission.
	 *
	 * @internal
	 *
	 * @param Submission_Interface $submission The quiz submission.
	 *
	 * @throws RuntimeException In case the lesson status is missing.
	 */
	public function save( Submission_Interface $submission ): void {
		$status_comment = $this->get_status_comment( $submission->get_quiz_id(), $submission->get_user_id() );

		if ( ! $status_comment ) {
			throw new RuntimeException( 'Missing lesson status.' );
		}

		if ( null !== $submission->get_final_grade() ) {
			update_comment_meta( $status_comment->comment_ID, 'grade', $submission->get_final_grade() );
		} else {
			delete_comment_meta( $status_comment->comment_ID, 'grade' );
		}
	}

	/**
	 * Delete the quiz submission.
	 *
	 * @internal
	 *
	 * @param Submission_Interface $submission The quiz submission.
	 */
	public function delete( Submission_Interface $submission ): void {
		delete_comment_meta( $submission->get_id(), 'grade' );
	}

	/**
	 * Get the lesson status comment.
	 *
	 * @param int $quiz_id The quiz ID.
	 * @param int $user_id The user ID.
	 *
	 * @return WP_Comment|null The comment instance or null if not found.
	 */
	private function get_status_comment( int $quiz_id, int $user_id ): ?WP_Comment {
		$lesson_id = Sensei()->quiz->get_lesson_id( $quiz_id );

		$status_comment = Sensei_Utils::user_lesson_status( $lesson_id, $user_id );

		if ( ! $status_comment ) {
			return null;
		}

		return $status_comment;
	}

	/**
	 * Get the submission creation date by using the status comment start date.
	 * The status comment start date is not the real submission creation date,
	 * but this is the best we have.
	 *
	 * @param WP_Comment $status_comment The lesson status comment.
	 *
	 * @return DateTimeInterface The created date.
	 */
	private function get_created_date( WP_Comment $status_comment ): DateTimeInterface {
		$start_date = get_comment_meta( $status_comment->comment_ID, 'start', true );

		return $start_date
			? new DateTimeImmutable( $start_date, wp_timezone() )
			: new DateTimeImmutable( 'now', wp_timezone() );
	}
}