Open Menu

Exercise Solutions#^ TOP

Every CGI & CLI type exercise must have a reference solution. The solution represents a complete working example of how to solve the exercise's problem.

The solution is used for a few things in the workshop:

  • When verifying a student's solution to an exercise, the reference solution is invoked and the output of the two are compared.
  • To display to the student as the proposed solution after they have completed the exercise. This is useful if they solved the problem in a less optimal albeit working way.

A solution is represented by an implementation of PhpSchool\PhpWorkshop\Solution\SolutionInterface.

The workshop framework ships with two implementations of PhpSchool\PhpWorkshop\Solution\SolutionInterface to cover most scenarios:

  • PhpSchool\PhpWorkshop\Solution\SingleFileSolution - When your reference solution is just one file. For example solution.php
  • PhpSchool\PhpWorkshop\Solution\DirectorySolution - When your reference solution comprises of multiple files.

SingleFileSolution#^ TOP

This is the default used by PhpSchool\PhpWorkshop\Exercise\AbstractExercise. So if you don't override the getSolution() method, the solution will be a single file named solution.php contained in the directory exercises/exercise-name/solution.

This would look like the following if you were to manually construct it:

<?php
require __DIR__ . '/vendor/autoload.php';

use PhpSchool\PhpWorkshop\Solution\SingleFileSolution;

$solution = SingleFileSolution::fromFile('/path/to/workshop/exercises/exercise-name/solution.php');

var_dump($solution->getEntryPoint());
//Outputs: "/path/to/workshop/exercises/exercise-name/solution.php"

var_dump($solution->getBaseDirectory());
//Outputs: "/path/to/workshop/exercises/exercise-name"

var_dump($solution->hasComposerFile());
//Outputs: "false";

$files = $solution->getFiles();

var_dump(count($files));
//Outputs: 1

$file = $files[0];

var_dump($file->getBaseDirectory());
//Outputs: "/path/to/workshop/exercises/exercise-name"

var_dump($file->getRelativePath());
//Outputs: "solution.php"

var_dump($file->__toString());
//Outputs: "/path/to/workshop/exercises/exercise-name/solution.php"

var_dump($file->getContents());
//Outputs: "contents-of-the-file"

DirectorySolution#^ TOP

It is possible that your solution contains more than one PHP file. Maybe you have some classes separated into different files, maybe you also pull in some dependencies via Composer. In either case, you should use PhpSchool\PhpWorkshop\Solution\DirectorySolution.

Usage is simple, just pass it the directory and an (optional) entry point. You can also provide an optional list of files to exclude, more on that later. The entry point defaults to solution.php. The following is a depiction of a directory structure and the code to encompass the solution:

/path/to/workshop/exercises/exercise-name/solution
├── SomeClass.php
└── solution.php

To return a directory solution when using the PhpSchool\PhpWorkshop\Exercise\AbstractExercise as a base for your exercise you can override the getSolution method with the following:

<?php

public function getSolution()
{
    return DirectorySolution::fromDirectory('/path/to/workshop/exercises/exercise-name');
}

This would load any files in the directory given and treat solution.php as the entry point. Constructed manually this might look like:

<?php
require __DIR__ . '/vendor/autoload.php';

use PhpSchool\PhpWorkshop\Solution\DirectorySolution;

$solution = DirectorySolution::fromDirectory('/path/to/workshop/exercises/exercise-name/solution');

var_dump($solution->getEntryPoint());
//Outputs: "/path/to/workshop/exercises/exercise-name/solution/solution.php"

var_dump($solution->getBaseDirectory());
//Outputs: "/path/to/workshop/exercises/exercise-name/solution"

var_dump($solution->hasComposerFile());
//Outputs: "false";

$files = $solution->getFiles();

var_dump(count($files));
//Outputs: 2

$file1 = $files[0];

var_dump($file1->getBaseDirectory());
//Outputs: "/path/to/workshop/exercises/exercise-name/solution"

var_dump($file1->getRelativePath());
//Outputs: "index.php"

var_dump($file1->__toString());
//Outputs: "/path/to/workshop/exercises/exercise-name/solution/solution.php"

var_dump($file1->getContents());
//Outputs: "contents-of-the-file"

$file2 = $files[1];

var_dump($file2->getBaseDirectory());
//Outputs: "/path/to/workshop/exercises/exercise-name/solution"

var_dump($file2->getRelativePath());
//Outputs: "SomeClass.php"

var_dump($file2->__toString());
//Outputs: "/path/to/workshop/exercises/exercise-name/solution/SomeClass.php"

var_dump($file2->getContents());
//Outputs: "contents-of-the-file"

Using a different entry point file

If your solution looked like the below where your entry point is named index.php, you can provide the optional third parameter to the static constructor: fromDirectory. It must be the relative path of the file from the solution base directory.

/path/to/workshop/exercises/exercise-name
├── SomeClass.php
└── index.php
<?php
require __DIR__ . '/vendor/autoload.php';

use PhpSchool\PhpWorkshop\Solution\DirectorySolution;

$solution = DirectorySolution::fromDirectory('/path/to/workshop/exercises/exercise-name', [], 'index.php');

The convention is for the entry point file to be named solution.php to keep things simple.

DirectorySolution will throw an instance of InvalidArgumentException if the entry point does not exist in the directory given.

Excluding files from the solution directory

The method getFiles is used to find all the files in an solution. One use case is to display the contents of the files to the student when they have finished an exercise. This way they can compare notes. Sometimes you may want some files to be excluded from this. Perhaps you don't want the composer.lock file to be printed to the terminal as this can be quite long. To exclude some files from the solution, simply provide an array of excludes, relative to the base directory:

<?php
require __DIR__ . '/vendor/autoload.php';

use PhpSchool\PhpWorkshop\Solution\DirectorySolution;

$solution = DirectorySolution::fromDirectory(
    '/path/to/workshop/exercises/exercise-name/solution',
    [
        'composer.lock',
        'vendor'
    ]
);

It is not actually necessary to exclude composer.lock or vendor as these are automatically appended to the list of excludes when using the static constructor fromDirectory.

Overriding the default excluded files

The following files are excluded by default when using the static constructor fromDirectory:

  • composer.lock
  • vendor

If for some reason you do not want to ignore, say composer.lock but still vendor you can use the __construct method which does not have any default values:

<?php
require __DIR__ . '/vendor/autoload.php';

use PhpSchool\PhpWorkshop\Solution\DirectorySolution;

$solution = new DirectorySolution(
    '/path/to/workshop/exercises/exercise-name/solution',
    'solution.php',
    ['vendor']
);

The argument order for __construct and fromDirectory are slightly different. __construct is: $directory, $entryPoint, $excludes. fromDirectory is: $directory, $excludes, $entryPoint.

Using Composer Libraries in your solution

If you use a library via Composer then you should include the composer.json and composer.lock file in the solution base directory. DirectorySolution will detect the Composer files automatically. If there are Composer files available, the workshop will run a composer install in the solution base directory before invoking the solution.

This means that you don't need to commit the vendor directory for each reference solution.

Creating your own Exercise Solution Type#^ TOP

If the SingleFileSolution or DirectorySolution implementations do not cover your needs, you can create your own by implementing the following interface:

interface SolutionInterface
{
    /**
     * @return string
     */
    public function getEntryPoint();

    /**
     * @return SolutionFile[]
     */
    public function getFiles();

    /**
     * @return string
     */
    public function getBaseDirectory();

    /**
     * @return bool
     */
    public function hasComposerFile();
}

getEntryPoint()

This method should return the name of the file which should be the entry point to your solution, in absolute form.

getFiles()

This method should return an array of files. Each file should be represented by an instance of PhpSchool\PhpWorkshop\Solution\SolutionFile.

getBaseDirectory()

This should return the absolute path to the directory of the solution.

hasComposerFile()

This should return a boolean value depending on whether the solution has a composer.lock file present. If it does, before invoking the solution, composer install will be executed in the solution base directory. This saves you having to bundle the vendor directory in your workshop.