Ready for the next step?
Use the PHP School workshop documentation to build you own workshop and help teach others PHP!
Open Menu
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.
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!
There are two type of modifications you can do to a 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 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 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.
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.