Open Menu

Patching Exercise Submissions#^ TOP

This is the fun bit! - In this article we will show how we can modify the student's solution, injecting, modifying and wrapping code. Before we get down to it, a little background to explain why we built this feature.

Why?

We wanted a way to make sure that display_errors and error_reporting were always configured correctly, we also thought that we might want to wrap solutions in try/catch blocks so we could give more structured feedback to the student. We figured, in order to do this in a robust manner, we would have to patch the student's solution on the fly and revert the changes after the framework has verified/run the solution.

We decided this feature may be useful for workshop developers, we thought there may be possibilities where you want to concentrate on a verify specific problem like "Here is a variable - transform it to this", well with this feature, you could inject that variable at the start of the script so it is already available to the student!

How?

There are two type of modifications you can do to a solution:

  • Insertion (Insert code at the beginning or end of a solution)
  • Transformer (Use a callable to modify an AST representation of the solution)

In order to inform the workshop framework that an exercise would like to patch a solution it must implement PhpSchool\PhpWorkshop\Exercise\SubmissionPatchable and return an instance of PhpSchool\PhpWorkshop\Patch.

The Patch object is where you specify your Insertions & Transformers. The API looks like this:

<?php

use PhpSchool\PhpWorkshop\Patch;

$patch = new Patch;
$patch = $patch->withInsertion($insertion1);
$patch = $patch->withInsertion($insertion2);
$patch = $patch->withTransformer($transformer);

The Patch class is immutable so you will need to assign the result of any calls to with* methods.

Insertions#^ TOP

Insertions allow to insert a block of code at either the beginning or end of the student's solution. The API is very simple:

<?php

use PhpSchool\PhpWorkshop\CodeInsertion;

$before = new CodeInsertion(CodeInsertion::TYPE_BEFORE, 'echo "Before";');
$after = new CodeInsertion(CodeInsertion::TYPE_AFTER, 'echo "After";');

Transformers#^ TOP

Transformers allow to modify the whole solution via an AST. A transformer is any valid PHP callable that returns an array of PhpParser\Node objects. The callable will be passed an array of PhpParser\Node objects which represent the parsed student's solution.

Lets see how you can build a transformer that wraps the solution in a try/catch block that then outputs the exception message.

<?php

use PhpSchool\PhpWorkshop\Exercise\SubmissionPatchable;
use PhpSchool\PhpWorkshop\Patch;
use PhpSchool\PhpWorkshop\CodeInsertion;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt\Catch_;
use PhpParser\Node\Stmt\Echo_;
use PhpParser\Node\Stmt\TryCatch;

class MyExercise extends AbstractExercise implements
    ExerciseInterface,
    SubmissionPatchable
{
    ...snip

    /**
     * @return Patch
     */
    public function getPatch()
    {
        $wrapInTryCatch = function (array $statements) {
                return [
                   new TryCatch(
                       $statements,
                       [
                           new Catch_(
                               new Name('Exception'),
                               'e',
                               [
                                   new Echo_([
                                       new MethodCall(new Variable('e'), 'getMessage')
                                   ])
                               ]
                           )
                       ]
                   )
                ];
            };

       return (new Patch)
            ->withTransformer($wrapInTryCatch);
    }
}

Note that the AST modification is fairly complicated, the feature is provided by the nikic/php-parser library and you should refer to that project for documentation on the AST.

Wiring it together#^ TOP

Let's write a patch that will wrap the solution in a try/catch block, add echo 'Start'; at the beginning and add echo 'Finish'; at the end:

<?php

use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt\Catch_;
use PhpParser\Node\Stmt\Echo_;
use PhpParser\Node\Stmt\TryCatch;
use PhpSchool\PhpWorkshop\CodeInsertion;
use PhpSchool\PhpWorkshop\Exercise\AbstractExercise;
use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface;
use PhpSchool\PhpWorkshop\Exercise\SubmissionPatchable;
use PhpSchool\PhpWorkshop\Patch;

class MyExercise extends AbstractExercise implements
    ExerciseInterface,
    SubmissionPatchable
{
    ...snip

    /**
     * @return Patch
     */
    public function getPatch()
    {
        $wrapInTryCatch = function (array $statements) {
                return [
                   new TryCatch(
                       $statements,
                       [
                           new Catch_(
                               new Name('Exception'),
                               'e',
                               [
                                   new Echo_([
                                       new MethodCall(new Variable('e'), 'getMessage')
                                   ])
                               ]
                           )
                       ]
                   )
                ];
            };

       return (new Patch)
           ->withTransformer($wrapInTryCatch)
           ->withInsertion(new CodeInsertion(CodeInsertion::TYPE_BEFORE, "echo 'Start';"))
           ->withInsertion(new CodeInsertion(CodeInsertion::TYPE_AFTER, "echo 'Finish';"));
    }
}

If the following solution was submitted:

<?php

echo "Hello World"
throw new InvalidArgumentException('What is this magic?');

Then the code that is actually invoked by the workshop framework would be the following:

<?php

echo 'Start';
try {
    echo "Hello World"
    throw new InvalidArgumentException('What is this magic?');
} catch (Exception $e) {
    echo $e->getMessage();
}
echo 'Finish';

The students solution will be reverted to the original form at the end of the verifying/running process so the student will never see the code in their solution file.