Open Menu

Creating an exercise#^ TOP

A workshop is a fairly useless without any exercises, so here we will learn how to create them.

It may be a good idea for exercises to start off simple and gradually increase in difficulty. You could try to explain concepts and build on them with each exercise.

Exercise checklist#^ TOP

  • Decide on a topic to teach.
  • Create a working reference solution.
  • Create a problem file.
  • Write the exercise.

We will decide on a topic of basic PHP operators, more specifically working out the mean average of a given set of numbers.

Exercise specification#^ TOP

  • The student should print out the mean average and nothing else. No new lines or whitespace.
  • The numbers passed to the program should be random.
  • The amount of numbers passed to the program should be random.
  • The numbers should be passed to the program as command line arguments.

Given this specification we could write a program which would serve as our reference solution.

<?php
$count = 0;
for ($i = 1; $i < count($argv); $i++) {
    $count += $argv[$i];
}

$numberCount = count($argv) - 1;
echo $count / $numberCount;

We should place this file in exercises/mean-average/solution/solution.php

Reference solutions are known, working programs which pass the exercise. When running a students's solution to an exercise, the reference solution is executed and the output compared to the student's.

The next step is to create a problem file. A problem file contains the instructions for the exercise. It should be a markdown file. This file is rendered in the terminal to the student when they select the exercise.

Tips for a good problem file#^ TOP

  • Provide a solid description of the problem.
  • Provide some sample code which may need to be modified.
  • Provide hints and tips.
  • Provide links to the PHP documentation and good articles from reputable sources regarding key areas of the problem.

Our problem file might look like the following.

Write a program that accepts one or more numbers as command-line arguments and prints the mean average of those numbers to the console (stdout).

----------------------------------------------------------------------
## HINTS

You can access command-line arguments via the global `$argv` array.

To get started, write a program that simply contains:

```php
var_dump($argv);
```

Run it with `php program.php` and some numbers as arguments. e.g:

```sh
$ php program.php 1 2 3
```

In which case the output would be an array looking something like:

```php
array(4) {
[0] =>
string(7) "program.php"
[1] =>
string(1) "1"
[2] =>
string(1) "2"
[3] =>
string(1) "3"
}
```

You'll need to think about how to loop through the number of arguments so you can output just their mean average. The first element of the `$argv` array is always the name of your script. eg `program.php`, so you need to start at the 2nd element (index 1), adding each item to the total until you reach the end of the array. You will then need to work out the average based on the amount of arguments given to you.

You can read how to work out an average here:
  [https://www.mathsisfun.com/mean.html]()

Also be aware that all elements of `$argv` are strings and you may need to *coerce* them into numbers. You can do this by prefixing the property with a cast `(int)` or just adding them. PHP will coerce it for you.

`{appname}` will be supplying arguments to your program when you run `{appname} verify program.php` so you don't need to supply them yourself. To test your program without verifying it, you can invoke it with `{appname} run program.php`. When you use `run`, you are invoking the test environment that `{appname}` sets up for each exercise.

----------------------------------------------------------------------

Any instances of {appname} will be replaced with the actual application name, this will most likely be the configuration you set when creating your workshop as this is inferred from the command the student executed to run the workshop.

Drop this file in exercises/mean-average/problem/problem.md.

Write the exercise#^ TOP

Now we write the code, there is not much to it, this is a simple exercise!

<?php

namespace PhpSchool\SimpleMath\Exercise;

use PhpSchool\PhpWorkshop\Exercise\AbstractExercise;
use PhpSchool\PhpWorkshop\Exercise\CliExercise;
use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface;
use PhpSchool\PhpWorkshop\Exercise\ExerciseType;

class Mean extends AbstractExercise implements ExerciseInterface, CliExercise
{

    /**
     * @return string
     */
    public function getName()
    {
        return 'Mean Average';
    }

    /**
     * @return string
     */
    public function getDescription()
    {
        return 'Simple Math';
    }

    /**
     * @return string[][]
     */
    public function getArgs()
    {
        $numArgs = rand(1, 10);

        $args = [];
        for ($i = 0; $i < $numArgs; $i ++) {
            $args[] = rand(0, 100);
        }

        return [$args];
    }

    /**
     * @return ExerciseType
     */
    public function getType()
    {
        return ExerciseType::CLI();
    }
}

Place the above in src/Exercise/Mean.php.

Now lets break this down.

This class represents our exercise, it describes how the programs will be executed, the student's and our reference solution.

AbstractExercise

The AbstractExercise class implements a few interesting methods for us. Mainly getSolution and getProblem. These methods are responsible for locating your solution and problem files. By default they take your exercise's name, normalise it (remove anything that is not A-Za-z or a dash, lowercase and replace spaces with dashes) and look in the exercises/<normalised-name>/solution and exercises/<normalised-name>/problem folders for files named solution.php and problem.md respectively. There maybe be cases when you need to override these methods, and in that case you probably don't need to extend from AbstractExercise.

You may need to override the methods getSolution and getProblem if you want to organise your problems and solutions in a different structure. We would advise against this in the name of consistency but if you have a good enough reason then the option is there. There may also be the case that your solution is not simply one file. Jump over to Exercise Solutions to learn more, if that is the case.

Exercise Type

Each exercise must have a type, there are currently two types of exercise: CGI, CLI & CUSTOM. Head over to Exercise Types to learn more. We are currently building a CLI type exercise, this means our reference solution and the student's solution programs will be invoked using the PHP CLI binary. The arguments will come from our exercise class. We inform the workshop of our exercise type by returning an instance of ExerciseType from the getType method. ExerciseType is an ENUM. In conjunction with this, our exercise should implement the respective interface. For CLI type exercises this is CliExercise.

CliExercise

This interface defines one method: getArgs. This method should return an array of arrays containing string arguments which will be passed to our reference solution and the student's solution at runtime. Each set of arguments will be sent to the solution. So you could essentially run the student's solution as many times as you wanted with different arguments. This method can return random records and random numbers of arguments so that each time the student runs the verification process they receive different arguments. This makes sure the solution is robust.

Try passing arguments which will test the boundaries of the student's solution, for example using minimum and maximum values and using random values on each invocation.

Do note that although your implementation of getArgs may return random arguments, your reference solution and the student's solution will always receive the same arguments as the getArgs method is only called once.

Our exercise simply returns one set of random number of arguments between 0 and 10, each being a random number between 0 and 100.

Name and description

The remaining methods to implement are getName and getDescription. getName is the name of the exercise to be displayed in the menu and getDescription is a short description of the exercise. This is not actually used anywhere yet but is useful when glancing through the code.

Registering the exercise and adding a factory#^ TOP

Internally, the workshop application uses a dependency injection container. This allows you to request other services from the application and replace services with your own implementations. In order for the application to locate your exercise, you need to register it with the application and also provide a factory for it. We use the PHP-DI package for dependency injection.

First, lets create a factory for our exercise. Open up app/config.php.

return [
    Mean::class => \DI\object(),
];

The file app/config.php should return an array of service definitions for the container. The key being the name of the service and the value the actual factory. For the case of exercises the service name should always be the class name. \DI\object() is a helper function to create a factory which will simply run new $yourClassName when asking for the service from the container.

See the section on The Container for more information on service definitions.

You are almost done! we have registered the factory which tells the application how to create your exercise. We just need to make it aware of your exercise. We do this in app/bootstrap.php. After the Application object is created you just call addExercise with the name of your exercise class. Your final app/bootstrap.php file should look something like the following:

<?php
ini_set('display_errors', 1);
date_default_timezone_set('Europe/London');
switch (true) {
    case (file_exists(__DIR__ . '/../vendor/autoload.php')):
        // Installed standalone
        require __DIR__ . '/../vendor/autoload.php';
        break;
    case (file_exists(__DIR__ . '/../../../autoload.php')):
        // Installed as a Composer dependency
        require __DIR__ . '/../../../autoload.php';
        break;
    case (file_exists('vendor/autoload.php')):
        // As a Composer dependency, relative to CWD
        require 'vendor/autoload.php';
        break;
    default:
        throw new RuntimeException('Unable to locate Composer autoloader; please run "composer install".');
}

use PhpSchool\PhpWorkshop\Application;
use PhpSchool\SimpleMath\Exercise\Mean;

$app = new Application('Simple Math', __DIR__ . '/config.php');

$app->addExercise(Mean::class);

$art = <<<ART
  ∞ ÷ ∑ ×

 PHP SCHOOL
SIMPLE MATH
ART;

$app->setLogo($art);
$app->setFgColour('red');
$app->setBgColour('black');

return $app;

That's it! You should now see your exercise in the menu when you run the app.

php bin/simple-math

You can compare your workshop against https://github.com/php-school/simple-math. This is a working copy of the tutorial workshop.