Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add quiz migration #7123

Merged
merged 13 commits into from
Sep 4, 2023
16 changes: 8 additions & 8 deletions includes/class-sensei.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@

use Sensei\Internal\Action_Scheduler\Action_Scheduler;
use Sensei\Internal\Emails\Email_Customization;
use Sensei\Internal\Installer\Migrations\Student_Progress_Migration;
use Sensei\Internal\Installer\Updates_Factory;
use Sensei\Internal\Migration\Migration_Tool;
use Sensei\Internal\Migration\Migration_Job;
use Sensei\Internal\Migration\Migration_Job_Scheduler;
use Sensei\Internal\Migration\Migrations\Quiz_Migration;
use Sensei\Internal\Migration\Migrations\Student_Progress_Migration;
use Sensei\Internal\Quiz_Submission\Answer\Repositories\Answer_Repository_Factory;
use Sensei\Internal\Quiz_Submission\Answer\Repositories\Answer_Repository_Interface;
use Sensei\Internal\Quiz_Submission\Grade\Repositories\Grade_Repository_Factory;
Expand All @@ -12,8 +16,6 @@
use Sensei\Internal\Quiz_Submission\Submission\Repositories\Submission_Repository_Interface;
use Sensei\Internal\Student_Progress\Course_Progress\Repositories\Course_Progress_Repository_Factory;
use Sensei\Internal\Student_Progress\Course_Progress\Repositories\Course_Progress_Repository_Interface;
use Sensei\Internal\Student_Progress\Jobs\Migration_Job;
use Sensei\Internal\Student_Progress\Jobs\Migration_Job_Scheduler;
use Sensei\Internal\Student_Progress\Lesson_Progress\Repositories\Lesson_Progress_Repository_Factory;
use Sensei\Internal\Student_Progress\Lesson_Progress\Repositories\Lesson_Progress_Repository_Interface;
use Sensei\Internal\Student_Progress\Quiz_Progress\Repositories\Quiz_Progress_Repository_Factory;
Expand All @@ -22,7 +24,6 @@
use Sensei\Internal\Student_Progress\Services\Lesson_Deleted_Handler;
use Sensei\Internal\Student_Progress\Services\Quiz_Deleted_Handler;
use Sensei\Internal\Student_Progress\Services\User_Deleted_Handler;
use Sensei\Internal\Student_Progress\Tools\Migration_Tool;

if ( ! defined( 'ABSPATH' ) ) {
exit;
Expand Down Expand Up @@ -593,10 +594,9 @@
$this->action_scheduler = new Action_Scheduler();
// Student progress migration.
if ( $use_tables ) {
$migration = new Student_Progress_Migration();
$migration_job = new Migration_Job( $migration );
$this->migration_scheduler = new Migration_Job_Scheduler( $this->action_scheduler, $migration_job );
$this->migration_scheduler->init();
$this->migration_scheduler = new Migration_Job_Scheduler( $this->action_scheduler );
$this->migration_scheduler->register_job( new Migration_Job( new Student_Progress_Migration() ) );
$this->migration_scheduler->register_job( new Migration_Job( new Quiz_Migration() ) );

Check warning on line 599 in includes/class-sensei.php

View check run for this annotation

Codecov / codecov/patch

includes/class-sensei.php#L598-L599

Added lines #L598 - L599 were not covered by tests
( new Migration_Tool( \Sensei_Tools::instance(), $this->migration_scheduler ) )->init();
}

Expand Down
66 changes: 66 additions & 0 deletions includes/internal/migration/class-migration-abstract.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php
/**
* File containing the abstract class for migrations.
*
* @package sensei
* @since $$next-version$$
*/

namespace Sensei\Internal\Migration;

/**
* Migration abstract class.
*
* @since $$next-version$$
*/
abstract class Migration_Abstract {
/**
* The errors that occurred during the migration.
*
* @var array
*/
private $errors = array();

/**
* The targeted plugin version.
*
* @since $$next-version$$
*
* @return string
*/
abstract public function target_version(): string;

/**
* Run the migration.
*
* @since $$next-version$$
*
* @param bool $dry_run Whether to run the migration in dry-run mode.
*
* @return int The number of rows migrated.
*/
abstract public function run( bool $dry_run = true );

/**
* Return the errors that occurred during the migration.
*
* @since $$next-version$$
*
* @return array
*/
public function get_errors(): array {
return $this->errors;

Check warning on line 52 in includes/internal/migration/class-migration-abstract.php

View check run for this annotation

Codecov / codecov/patch

includes/internal/migration/class-migration-abstract.php#L51-L52

Added lines #L51 - L52 were not covered by tests
}

/**
* Add an error message to the errors list unless it's there already.
*
* @param string $error The error message to add.
*/
protected function add_error( string $error ): void {
if ( ! in_array( $error, $this->errors, true ) ) {
$this->errors[] = $error;

Check warning on line 62 in includes/internal/migration/class-migration-abstract.php

View check run for this annotation

Codecov / codecov/patch

includes/internal/migration/class-migration-abstract.php#L60-L62

Added lines #L60 - L62 were not covered by tests
}
}
}

209 changes: 209 additions & 0 deletions includes/internal/migration/class-migration-job-scheduler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
<?php
/**
* File containing the Migration_Job_Scheduler class.
*
* @package sensei
*/

namespace Sensei\Internal\Migration;

use Sensei\Internal\Action_Scheduler\Action_Scheduler;

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

/**
* Class Migration_Job_Scheduler
*
* @internal
*
* @since $$next-version$$
*/
class Migration_Job_Scheduler {
/**
* Sensei jobs namespace.
*
* @var string
*/
private const HOOK_NAMESPACE = 'sensei_lms_migration_job_';

/**
* Migration errors option name.
*
* @var string
*/
public const ERRORS_OPTION_NAME = 'sensei_lms_migration_job_errors';

/**
* Migration job started option name.
*
* @var string
*/
public const STARTED_OPTION_NAME = 'sensei_lms_migration_job_started';

/**
* Migration job completed option name.
*
* @var string
*/
public const COMPLETED_OPTION_NAME = 'sensei_lms_migration_job_completed';

/**
* Action_Scheduler instance.
*
* @var Action_Scheduler
*/
private $action_scheduler;

/**
* Jobs to schedule.
*
* @var Migration_Job[]
*/
private $jobs = [];

/**
* Migration_Job_Scheduler constructor.
*
* @param Action_Scheduler $action_scheduler Action_Scheduler instance.
*/
public function __construct( Action_Scheduler $action_scheduler ) {
$this->action_scheduler = $action_scheduler;
}

/**
* Register a job to be scheduled.
*
* @param Migration_Job $job The migration job.
*/
public function register_job( Migration_Job $job ): void {
$this->jobs[ $job->get_name() ] = $job;

add_action( $this->get_job_hook_name( $job ), [ $this, 'run_job' ] );
}

/**
* Schedule all jobs.
*
* @internal
*
* @since $$next-version$$
* @throws \RuntimeException If no jobs to schedule.
*/
public function schedule(): void {
if ( ! $this->jobs ) {
throw new \RuntimeException( 'No jobs to schedule.' );
}

$first_job = reset( $this->jobs );

$this->schedule_job( $first_job );
}

/**
* Schedule a job.
*
* @param Migration_Job $job The migration job.
*/
private function schedule_job( Migration_Job $job ): void {
$this->action_scheduler->schedule_single_action(
$this->get_job_hook_name( $job ),
[ 'job_name' => $job->get_name() ],
false
);
}

/**
* Run the job.
*
* @internal
*
* @since $$next-version$$
*
* @param string $job_name The job name.
*/
public function run_job( string $job_name ): void {
if ( $this->is_first_run() ) {
$this->start();
}

$job = $this->jobs[ $job_name ];

$job->run();

if ( $job->get_errors() ) {
$migration_errors = (array) get_option( self::ERRORS_OPTION_NAME, [] );
$migration_errors = array_merge( $migration_errors, $job->get_errors() );
update_option( self::ERRORS_OPTION_NAME, $migration_errors );
}

if ( $job->is_complete() ) {
$next_job = $this->get_next_job( $job );
if ( $next_job ) {
$this->schedule_job( $next_job );
} else {
$this->complete();
}
} else {
$this->schedule_job( $job );
}
}

/**
* Get the next job.
*
* @param Migration_Job $job The migration job.
*
* @return Migration_Job|null
*/
private function get_next_job( Migration_Job $job ): ?Migration_Job {
$job_names = array_keys( $this->jobs );
$position = array_search( $job->get_name(), $job_names, true );
$has_next_job = false !== $position && isset( $job_names[ $position + 1 ] );

if ( ! $has_next_job ) {
return null;
}

return $this->jobs[ $job_names[ $position + 1 ] ];
}

/**
* Get the hook name for the job.
*
* @param Migration_Job $job The migration job.
*
* @return string
*/
private function get_job_hook_name( Migration_Job $job ): string {
return self::HOOK_NAMESPACE . $job->get_name();
}

/**
* Check if this is the first run of the job.
*
* @return bool
*/
private function is_first_run(): bool {
$started = get_option( self::STARTED_OPTION_NAME, 0 );
$completed = get_option( self::COMPLETED_OPTION_NAME, 0 );

return $started < $completed || 0 === $started;
}

/**
* Set start time.
*/
private function start(): void {
update_option( self::STARTED_OPTION_NAME, microtime( true ) );
delete_option( self::COMPLETED_OPTION_NAME );
}

/**
* Set completion time.
*/
private function complete(): void {
update_option( self::COMPLETED_OPTION_NAME, microtime( true ) );
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
* @package sensei
*/

namespace Sensei\Internal\Student_Progress\Jobs;
namespace Sensei\Internal\Migration;

use Sensei\Internal\Installer\Migrations\Student_Progress_Migration;
use ReflectionClass;

if ( ! defined( 'ABSPATH' ) ) {
exit;
Expand All @@ -23,9 +23,9 @@
class Migration_Job {

/**
* Progress migration.
* Migration.
*
* @var Student_Progress_Migration
* @var Migration_Abstract
*/
private $migration;

Expand All @@ -41,16 +41,18 @@ class Migration_Job {
*
* @var string
*/
private $job_name;
private $name;

/**
* Migration_Job constructor.
*
* @param Student_Progress_Migration $migration Progress migration.
* @param Migration_Abstract $migration Migration.
*/
public function __construct( Student_Progress_Migration $migration ) {
public function __construct( Migration_Abstract $migration ) {
$this->migration = $migration;
$this->job_name = 'progress_migration';
$this->name = strtolower(
( new ReflectionClass( $migration ) )->getShortName()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to replace this reflection with a public method in a migration.

  1. Reflection is more expensive. But in our case it isn't so important.
  2. That's an "implicit interface". Trying to find an analogy, I though up a terrible parallel that I don't want to share 😅 Anyway, I prefer explicit over implicit.
    What do you think?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes perfect sense. Fixed in e7332eb.

);
}

/**
Expand Down Expand Up @@ -100,8 +102,8 @@ public function is_complete(): bool {
*
* @return string
*/
public function get_job_name(): string {
return $this->job_name;
public function get_name(): string {
return $this->name;
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@
* @since 4.16.1
*/

namespace Sensei\Internal\Student_Progress\Tools;
namespace Sensei\Internal\Migration;

use Sensei\Internal\Student_Progress\Jobs\Migration_Job_Scheduler;
use Sensei_Tools;

if ( ! defined( 'ABSPATH' ) ) {
Expand Down
Loading
Loading