Source: includes/internal/student-progress/quiz-progress/repositories/class-tables-based-quiz-progress-repository.php

<?php
/**
 * File containing the class \Sensei\Student_Progress\Quiz_Progress\Repositories\Tables_Based_Quiz_Progress_Repository.
 *
 * @package sensei
 */

namespace Sensei\Internal\Student_Progress\Quiz_Progress\Repositories;

use DateTimeImmutable;
use DateTimeZone;
use Sensei\Internal\Student_Progress\Quiz_Progress\Models\Quiz_Progress_Interface;
use Sensei\Internal\Student_Progress\Quiz_Progress\Models\Tables_Based_Quiz_Progress;
use wpdb;

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

/**
 * Class Tables_Based_Quiz_Progress_Repository
 *
 * @internal
 *
 * @since 4.16.1
 */
class Tables_Based_Quiz_Progress_Repository implements Quiz_Progress_Repository_Interface {
	/**
	 * WordPress database object.
	 *
	 * @var wpdb
	 */
	private $wpdb;

	/**
	 * Tables_Based_Course_Progress_Repository constructor.
	 *
	 * @internal
	 *
	 * @param wpdb $wpdb WordPress database object.
	 */
	public function __construct( wpdb $wpdb ) {
		$this->wpdb = $wpdb;
	}

	/**
	 * Create a new quiz progress.
	 *
	 * @internal
	 *
	 * @param int $quiz_id Quiz identifier.
	 * @param int $user_id User identifier.
	 * @return Quiz_Progress_Interface
	 */
	public function create( int $quiz_id, int $user_id ): Quiz_Progress_Interface {
		/**
		 * Filter quiz id for quiz progress creation.
		 *
		 * @hook sensei_quiz_progress_create_quiz_id
		 *
		 * @since 4.23.1
		 *
		 * @param {int} $quiz_id Quiz ID.
		 * @return {int} Filtered quiz ID.
		 */
		$quiz_id = (int) apply_filters( 'sensei_quiz_progress_create_quiz_id', $quiz_id );

		$current_datetime = new DateTimeImmutable( 'now', new DateTimeZone( 'UTC' ) );
		$date_format      = 'Y-m-d H:i:s';
		$this->wpdb->insert(
			$this->wpdb->prefix . 'sensei_lms_progress',
			[
				'post_id'        => $quiz_id,
				'user_id'        => $user_id,
				'parent_post_id' => null,
				'type'           => 'quiz',
				'status'         => Quiz_Progress_Interface::STATUS_IN_PROGRESS,
				'started_at'     => $current_datetime->format( $date_format ),
				'completed_at'   => null,
				'created_at'     => $current_datetime->format( $date_format ),
				'updated_at'     => $current_datetime->format( $date_format ),
			],
			[
				'%d',
				'%d',
				null,
				'%s',
				'%s',
				'%s',
				null,
				'%s',
				'%s',
			]
		);
		$id = (int) $this->wpdb->insert_id;

		return new Tables_Based_Quiz_Progress(
			$id,
			$quiz_id,
			$user_id,
			Quiz_Progress_Interface::STATUS_IN_PROGRESS,
			$current_datetime,
			null,
			$current_datetime,
			$current_datetime
		);
	}

	/**
	 * Find a quiz progress by quiz and user identifiers.
	 *
	 * @internal
	 *
	 * @param int $quiz_id Quiz identifier.
	 * @param int $user_id User identifier.
	 * @return Quiz_Progress_Interface|null
	 */
	public function get( int $quiz_id, int $user_id ): ?Quiz_Progress_Interface {
		if ( ! $user_id ) {
			return null;
		}

		/**
		 * Filter quiz id for quiz progress retrieval.
		 *
		 * @hook sensei_quiz_progress_get_quiz_id
		 *
		 * @since 4.23.1
		 *
		 * @param {int} $quiz_id Quiz ID.
		 * @return {int} Filtered quiz ID.
		 */
		$quiz_id = (int) apply_filters( 'sensei_quiz_progress_get_quiz_id', $quiz_id );

		$table_name = $this->wpdb->prefix . 'sensei_lms_progress';
		$query      = $this->wpdb->prepare(
			// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
			'SELECT * FROM ' . $table_name . ' WHERE post_id = %d AND user_id = %d AND type = %s',
			$quiz_id,
			$user_id,
			'quiz'
		);

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$row = $this->wpdb->get_row( $query );
		if ( ! $row ) {
			return null;
		}

		$timezone = new DateTimeZone( 'UTC' );

		return new Tables_Based_Quiz_Progress(
			(int) $row->id,
			(int) $row->post_id,
			(int) $row->user_id,
			$row->status,
			new DateTimeImmutable( $row->started_at, $timezone ),
			$row->completed_at ? new DateTimeImmutable( $row->completed_at, $timezone ) : null,
			new DateTimeImmutable( $row->created_at, $timezone ),
			new DateTimeImmutable( $row->updated_at, $timezone )
		);
	}

	/**
	 * Check if a quiz progress exists.
	 *
	 * @internal
	 *
	 * @param int $quiz_id Quiz identifier.
	 * @param int $user_id User identifier.
	 * @return bool
	 */
	public function has( int $quiz_id, int $user_id ): bool {
		if ( ! $user_id ) {
			return false;
		}

		/**
		 * Filter quiz id for quiz progress existence check.
		 *
		 * @hook sensei_quiz_progress_has_quiz_id
		 *
		 * @since 4.23.1
		 *
		 * @param {int} $quiz_id Quiz ID.
		 * @return {int} Filtered quiz ID.
		 */
		$quiz_id = (int) apply_filters( 'sensei_quiz_progress_has_quiz_id', $quiz_id );

		$table_name = $this->wpdb->prefix . 'sensei_lms_progress';
		$query      = $this->wpdb->prepare(
			// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
			'SELECT COUNT(*) FROM ' . $table_name . ' WHERE post_id = %d AND user_id = %d AND type = %s',
			$quiz_id,
			$user_id,
			'quiz'
		);

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$count = (int) $this->wpdb->get_var( $query );

		return $count > 0;
	}

	/**
	 * Save the quiz progress.
	 *
	 * @internal
	 *
	 * @param Quiz_Progress_Interface $quiz_progress Quiz progress.
	 */
	public function save( Quiz_Progress_Interface $quiz_progress ): void {
		$this->assert_tables_based_quiz_progress( $quiz_progress );

		$updated_at = new DateTimeImmutable( 'now', new DateTimeZone( 'UTC' ) );
		$quiz_progress->set_updated_at( $updated_at );

		$date_format = 'Y-m-d H:i:s';
		$this->wpdb->update(
			$this->wpdb->prefix . 'sensei_lms_progress',
			[
				'status'       => $quiz_progress->get_status(),
				'started_at'   => $quiz_progress->get_started_at() ? $quiz_progress->get_started_at()->format( $date_format ) : null,
				'completed_at' => $quiz_progress->get_completed_at() ? $quiz_progress->get_completed_at()->format( $date_format ) : null,
				'updated_at'   => $quiz_progress->get_updated_at()->format( $date_format ),
			],
			[
				'id' => $quiz_progress->get_id(),
			],
			[
				'%s',
				'%s',
				$quiz_progress->get_completed_at() ? '%s' : null,
				'%s',
			],
			[
				'%d',
			]
		);
	}

	/**
	 * Delete a quiz progress.
	 *
	 * @internal
	 *
	 * @param Quiz_Progress_Interface $quiz_progress Quiz progress.
	 */
	public function delete( Quiz_Progress_Interface $quiz_progress ): void {
		$this->wpdb->delete(
			$this->wpdb->prefix . 'sensei_lms_progress',
			[
				'post_id' => $quiz_progress->get_quiz_id(),
				'user_id' => $quiz_progress->get_user_id(),
				'type'    => 'quiz',
			],
			[
				'%d',
				'%d',
				'%s',
			]
		);
	}

	/**
	 * Delete all quiz progress for a quiz.
	 *
	 * @internal
	 *
	 * @param int $quiz_id Quiz identifier.
	 */
	public function delete_for_quiz( int $quiz_id ): void {
		/**
		 * Filter quiz id for quiz progress deletion.
		 *
		 * @hook sensei_quiz_progress_delete_for_quiz_quiz_id
		 *
		 * @since 4.23.1
		 *
		 * @param {int} $quiz_id Quiz ID.
		 * @return {int} Filtered quiz ID.
		 */
		$quiz_id = (int) apply_filters( 'sensei_quiz_progress_delete_for_quiz_quiz_id', $quiz_id );

		$this->wpdb->delete(
			$this->wpdb->prefix . 'sensei_lms_progress',
			[
				'post_id' => $quiz_id,
				'type'    => 'quiz',
			],
			[
				'%d',
				'%s',
			]
		);
	}

	/**
	 * Delete all quiz progress for a user.
	 *
	 * @internal
	 *
	 * @param int $user_id User identifier.
	 */
	public function delete_for_user( int $user_id ): void {
		$this->wpdb->delete(
			$this->wpdb->prefix . 'sensei_lms_progress',
			[
				'user_id' => $user_id,
				'type'    => 'quiz',
			],
			[
				'%d',
				'%s',
			]
		);
	}

	/**
	 * Assert that the quiz progress is a Tables_Based_Quiz_Progress.
	 *
	 * @param Quiz_Progress_Interface $quiz_progress Quiz progress.
	 * @throws \InvalidArgumentException If the quiz progress is not a Tables_Based_Quiz_Progress.
	 */
	private function assert_tables_based_quiz_progress( Quiz_Progress_Interface $quiz_progress ): void {
		if ( ! $quiz_progress instanceof Tables_Based_Quiz_Progress ) {
			$actual_type = get_class( $quiz_progress );
			throw new \InvalidArgumentException( esc_html( "Expected Tables_Based_Quiz_Progress, got {$actual_type}." ) );
		}
	}

	/**
	 * Find quiz progress.
	 *
	 * @internal
	 *
	 * @param array $args The arguments.
	 * @return Quiz_Progress_Interface[]
	 */
	public function find( array $args ): array {
		$quiz_id = $args['quiz_id'] ?? null;
		$user_id = $args['user_id'] ?? null;
		$status  = $args['status'] ?? null;
		$limit   = $args['number'] ?? 100;
		$offset  = $args['offset'] ?? 0;

		$where_clause = array( 'type = %s' );
		$query_params = array( 'quiz' );
		if ( ! empty( $quiz_id ) ) {
			$quiz_id = (array) $quiz_id;
			$quiz_id = array_map( 'intval', $quiz_id );
			$quiz_id = array_map(
				function ( $id ) {
					/**
					 * Filter quiz id for quiz progress retrieval.
					 *
					 * @hook sensei_quiz_progress_find_quiz_id
					 *
					 * @since 4.23.1
					 *
					 * @param {int} $quiz_id Quiz ID.
					 * @return {int} Filtered quiz ID.
					 */
					return (int) apply_filters( 'sensei_quiz_progress_find_quiz_id', $id );
				},
				$quiz_id
			);

			$query_params   = array_merge( $query_params, $quiz_id );
			$where_clause[] = 'post_id IN (' . $this->get_placeholders( $quiz_id ) . ')';
		}

		if ( ! empty( $user_id ) ) {
			$query_params[] = (int) $user_id;
			$where_clause[] = 'user_id = %d';
		}

		if ( ! empty( $status ) ) {
			$query_params   = array_merge( $query_params, (array) $status );
			$where_clause[] = 'status IN (' . $this->get_placeholders( (array) $status ) . ')';
		}

		$table_name = $this->wpdb->prefix . 'sensei_lms_progress';

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$query_string = 'SELECT * FROM ' . $table_name . ' ';
		if ( count( $where_clause ) > 0 ) {
			$query_string .= 'WHERE ' . implode( ' AND ', $where_clause ) . ' ';
		}

		$query_string  .= 'ORDER BY id ASC ';
		$query_string  .= 'LIMIT %d OFFSET %d';
		$query_params[] = $limit;
		$query_params[] = $offset;

		$query = $this->wpdb->prepare(
			$query_string, // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
			...$query_params
		);

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$rows = $this->wpdb->get_results( $query );
		if ( ! $rows ) {
			return array();
		}

		$timezone = new DateTimeZone( 'UTC' );

		$course_progresses = array();
		foreach ( $rows as $row ) {
			$course_progresses[] = new Tables_Based_Quiz_Progress(
				(int) $row->id,
				(int) $row->post_id,
				(int) $row->user_id,
				$row->status,
				$row->started_at ? new DateTimeImmutable( $row->started_at, $timezone ) : null,
				$row->completed_at ? new DateTimeImmutable( $row->completed_at, $timezone ) : null,
				new DateTimeImmutable( $row->created_at, $timezone ),
				new DateTimeImmutable( $row->updated_at, $timezone )
			);
		}

		return $course_progresses;
	}

	/**
	 * Return a string of placeholders for the given values.
	 *
	 * @param array $values The values.
	 * @return string The placeholders.
	 */
	private function get_placeholders( array $values ) {
		if ( empty( $values ) ) {
			return '';
		}

		$placeholder  = is_numeric( $values[0] ) ? '%d' : '%s';
		$placeholders = array_fill( 0, count( $values ), $placeholder );

		return implode( ', ', $placeholders );
	}
}