<?php
/**
* File containing the Comments_Based_Lesson_Progress_Repository class.
*
* @package sensei
*/
namespace Sensei\Internal\Student_Progress\Lesson_Progress\Repositories;
use DateTime;
use InvalidArgumentException;
use ReflectionClass;
use RuntimeException;
use Sensei\Internal\Student_Progress\Lesson_Progress\Models\Comments_Based_Lesson_Progress;
use Sensei\Internal\Student_Progress\Lesson_Progress\Models\Lesson_Progress_Interface;
use Sensei_Utils;
use WP_Comment;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class Comments_Based_Lesson_Progress_Repository.
*
* @internal
*
* @since 4.7.2
*/
class Comments_Based_Lesson_Progress_Repository implements Lesson_Progress_Repository_Interface {
/**
* Creates a new lesson progress.
*
* @internal
*
* @param int $lesson_id The lesson ID.
* @param int $user_id The user ID.
*
* @return Lesson_Progress_Interface The lesson progress.
* @throws RuntimeException When the lesson progress could not be created.
*/
public function create( int $lesson_id, int $user_id ): Lesson_Progress_Interface {
/**
* Filter lesson id for lesson progress creation.
*
* @hook sensei_lesson_progress_create_lesson_id
*
* @since 4.23.1
*
* @param {int} $lesson_id The lesson ID.
* @return {int} Filtered lesson ID.
*/
$lesson_id = (int) apply_filters( 'sensei_lesson_progress_create_lesson_id', $lesson_id );
$metadata = [
'start' => current_time( 'mysql' ),
];
$comment_id = Sensei_Utils::update_lesson_status( $user_id, $lesson_id, Lesson_Progress_Interface::STATUS_IN_PROGRESS, $metadata );
if ( ! $comment_id ) {
throw new RuntimeException( "Can't create a lesson progress" );
}
$comment = get_comment( $comment_id );
if ( ! $comment ) {
throw new RuntimeException( 'Created lesson progress not found' );
}
return $this->create_progress_from_comment( $comment );
}
/**
* Finds a lesson progress by lesson and user.
*
* @internal
*
* @param int $lesson_id The lesson ID.
* @param int $user_id The user ID.
*
* @return Lesson_Progress_Interface|null The lesson progress or null if not found.
*/
public function get( int $lesson_id, int $user_id ): ?Lesson_Progress_Interface {
if ( ! $user_id ) {
return null;
}
/**
* Filter lesson id for lesson progress creation.
*
* @hook sensei_lesson_progress_get_lesson_id
*
* @since 4.23.1
*
* @param {int} $lesson_id The lesson ID.
* @return {int} Filtered lesson ID.
*/
$lesson_id = (int) apply_filters( 'sensei_lesson_progress_get_lesson_id', $lesson_id );
$activity_args = [
'post_id' => $lesson_id,
'user_id' => $user_id,
'type' => 'sensei_lesson_status',
];
$comment = Sensei_Utils::sensei_check_for_activity( $activity_args, true );
if ( ! $comment instanceof WP_Comment ) {
return null;
}
return $this->create_progress_from_comment( $comment );
}
/**
* Check if a lesson progress exists.
*
* @internal
*
* @param int $lesson_id The lesson ID.
* @param int $user_id The user ID.
* @return bool
*/
public function has( int $lesson_id, int $user_id ): bool {
if ( ! $user_id ) {
return false;
}
/**
* Filter lesson id for lesson progress check.
*
* @hook sensei_lesson_progress_has_lesson_id
*
* @since 4.23.1
*
* @param {int} $lesson_id The lesson ID.
* @return {int} Filtered lesson ID.
*/
$lesson_id = (int) apply_filters( 'sensei_lesson_progress_has_lesson_id', $lesson_id );
$activity_args = [
'post_id' => $lesson_id,
'user_id' => $user_id,
'type' => 'sensei_lesson_status',
];
$count = Sensei_Utils::sensei_check_for_activity( $activity_args );
return $count > 0;
}
/**
* Save the lesson progress.
*
* @internal
*
* @param Lesson_Progress_Interface $lesson_progress The lesson progress.
*/
public function save( Lesson_Progress_Interface $lesson_progress ): void {
$this->assert_comments_based_lesson_progress( $lesson_progress );
$metadata = [];
if ( $lesson_progress->get_started_at() ) {
$metadata['start'] = $lesson_progress->get_started_at()->format( 'Y-m-d H:i:s' );
}
// We need to use internal value for status, not the one returned by the getter.
// Comments_Based_Lesson_Progress::get_status() returns a normalized status, but we need the internal one.
$reflection_class = new ReflectionClass( Comments_Based_Lesson_Progress::class );
$status_property = $reflection_class->getProperty( 'status' );
$status_property->setAccessible( true );
$status = (string) $status_property->getValue( $lesson_progress );
$comment_id = (int) Sensei_Utils::update_lesson_status(
$lesson_progress->get_user_id(),
$lesson_progress->get_lesson_id(),
$status,
$metadata
);
if ( $lesson_progress->is_complete() && $comment_id ) {
$comment = [
'comment_ID' => $comment_id,
'comment_date' => $lesson_progress->get_completed_at()->format( 'Y-m-d H:i:s' ),
];
wp_update_comment( $comment );
Sensei()->flush_comment_counts_cache( $lesson_progress->get_lesson_id() );
}
}
/**
* Delete the lesson progress.
*
* @internal
*
* @param Lesson_Progress_Interface $lesson_progress The lesson progress.
*/
public function delete( Lesson_Progress_Interface $lesson_progress ): void {
$args = array(
'post_id' => $lesson_progress->get_lesson_id(),
'type' => 'sensei_lesson_status',
'user_id' => $lesson_progress->get_user_id(),
);
// This auto deletes the corresponding meta data, such as the quiz grade, and questions asked.
Sensei_Utils::sensei_delete_activities( $args );
}
/**
* Delete all lesson progress for a lesson.
* This is used when a lesson is deleted.
*
* @internal
*
* @param int $lesson_id The lesson ID.
*/
public function delete_for_lesson( int $lesson_id ): void {
/**
* Filter lesson id for lesson progress deletion.
*
* @hook sensei_lesson_progress_delete_for_lesson_lesson_id
*
* @since 4.23.1
*
* @param {int} $lesson_id The lesson ID.
* @return {int} Filtered lesson ID.
*/
$lesson_id = (int) apply_filters( 'sensei_lesson_progress_delete_for_lesson_lesson_id', $lesson_id );
$args = array(
'post_id' => $lesson_id,
'type' => 'sensei_lesson_status',
);
$this->delete_activities( $args );
}
/**
* Delete all lesson progress for a user.
* This is used when a user is deleted.
*
* @internal
*
* @param int $user_id The user ID.
*/
public function delete_for_user( int $user_id ): void {
$args = array(
'user_id' => $user_id,
'type' => 'sensei_lesson_status',
);
$this->delete_activities( $args );
}
/**
* Returns the number of started lessons for a user in a course.
* The number of started lessons is the same as the number of lessons that have a progress record.
*
* @internal
*
* @param int $course_id The course ID.
* @param int $user_id The user ID.
* @return int
*/
public function count( int $course_id, int $user_id ): int {
if ( ! $user_id ) {
return 0;
}
/**
* Filter course id for lesson progress counting.
*
* @hook sensei_lesson_progress_count_course_id
*
* @since 4.23.1
*
* @param {int} $course_id The course ID.
* @return {int} Filtered course ID.
*/
$course_id = (int) apply_filters( 'sensei_lesson_progress_count_course_id', $course_id );
$lessons = Sensei()->course->course_lessons( $course_id, 'publish', 'ids' );
if ( empty( $lessons ) ) {
return 0;
}
$activity_args = array(
'post__in' => $lessons,
'user_id' => $user_id,
'type' => 'sensei_lesson_status',
);
return (int) Sensei_Utils::sensei_check_for_activity( $activity_args );
}
/**
* Delete activity comments by given arguments.
*
* @param array $args Arguments to delete activity comments.
*/
private function delete_activities( array $args ): void {
$comments = Sensei_Utils::sensei_check_for_activity( $args, true );
if ( ! $comments ) {
return;
}
$comments = is_array( $comments ) ? $comments : [ $comments ];
$post_ids = [];
foreach ( $comments as $comment ) {
if ( isset( $comment->comment_post_ID ) ) {
$post_ids[] = $comment->comment_post_ID;
}
if ( isset( $comment->comment_ID ) && 0 < $comment->comment_ID ) {
wp_delete_comment( intval( $comment->comment_ID ), true );
}
}
foreach ( $post_ids as $post_id ) {
Sensei()->flush_comment_counts_cache( $post_id );
}
}
/**
* Asserts that the lesson progress is a Comments_Based_Lesson_Progress.
*
* @param Lesson_Progress_Interface $lesson_progress The lesson progress.
* @throws InvalidArgumentException When the lesson progress is not a Comments_Based_Lesson_Progress.
*/
private function assert_comments_based_lesson_progress( Lesson_Progress_Interface $lesson_progress ): void {
if ( ! $lesson_progress instanceof Comments_Based_Lesson_Progress ) {
$actual_type = get_class( $lesson_progress );
throw new InvalidArgumentException( esc_html( "Expected Comments_Based_Lesson_Progress, got {$actual_type}." ) );
}
}
/**
* Find lesson progress.
*
* @internal
*
* @param array $args The arguments.
* @return Lesson_Progress_Interface[]
* @throws InvalidArgumentException When the ordering is not supported.
*/
public function find( array $args ): array {
$comments_args = array(
'type' => 'sensei_lesson_status',
'order' => 'ASC',
'orderby' => 'comment_ID',
);
if ( isset( $args['lesson_id'] ) ) {
$lesson_ids = array_map( 'intval', (array) $args['lesson_id'] );
$lesson_ids = array_map(
function ( $lesson_id ) {
/**
* Filter lesson id when finding lesson progress.
*
* @hook sensei_lesson_progress_find_lesson_id
*
* @since 4.23.1
*
* @param {int} $lesson_id The lesson ID.
* @return {int} Filtered lesson ID.
*/
return (int) apply_filters( 'sensei_lesson_progress_find_lesson_id', $lesson_id );
},
$lesson_ids
);
$comments_args['post__in'] = $lesson_ids;
}
if ( isset( $args['user_id'] ) ) {
$comments_args['user_id'] = $args['user_id'];
}
if ( isset( $args['status'] ) ) {
$comments_args['status'] = $args['status'];
}
if ( isset( $args['order'] ) ) {
$comments_args['order'] = $args['order'];
}
if ( isset( $args['orderby'] ) ) {
switch ( $args['orderby'] ) {
case 'started_at':
throw new InvalidArgumentException( 'Ordering by started_at is not supported in comments-based version.' );
case 'completed_at':
case 'created_at':
case 'updated_at':
$comments_args['orderby'] = 'comment_date';
break;
case 'lesson_id':
$comments_args['orderby'] = 'comment_post_ID';
break;
case 'id':
$comments_args['orderby'] = 'comment_ID';
break;
case 'status':
$comments_args['orderby'] = 'comment_approved';
break;
default:
$comments_args['orderby'] = $args['orderby'];
break;
}
}
if ( isset( $args['order'] ) ) {
$comments_args['order'] = $args['order'];
}
if ( isset( $args['offset'] ) ) {
$comments_args['offset'] = $args['offset'];
}
if ( isset( $args['number'] ) ) {
$comments_args['number'] = $args['number'];
}
$comments = \Sensei_Utils::sensei_check_for_activity( $comments_args, true );
if ( empty( $comments ) ) {
return array();
}
$comments = is_array( $comments ) ? $comments : array( $comments );
$lesson_progresses = [];
foreach ( $comments as $comment ) {
$lesson_progresses[] = $this->create_progress_from_comment( $comment );
}
return $lesson_progresses;
}
/**
* Create a lesson progress from a comment.
*
* @param WP_Comment $comment The comment.
* @return Comments_Based_Lesson_Progress The course progress.
*/
private function create_progress_from_comment( WP_Comment $comment ): Comments_Based_Lesson_Progress {
$comment_date = new DateTime( $comment->comment_date, wp_timezone() );
$meta_start = get_comment_meta( (int) $comment->comment_ID, 'start', true );
$started_at = ! empty( $meta_start ) ? new DateTime( $meta_start, wp_timezone() ) : current_datetime();
if ( in_array( $comment->comment_approved, [ 'complete', 'passed', 'graded' ], true ) ) {
$completed_at = $comment_date;
} else {
$completed_at = null;
}
return new Comments_Based_Lesson_Progress(
(int) $comment->comment_ID,
(int) $comment->comment_post_ID,
(int) $comment->user_id,
$comment->comment_approved,
$started_at,
$completed_at,
$comment_date,
$comment_date
);
}
}