Plugins

Leo was designed to make adding functionality easy. All of Leo's functionality is derived from the Assertion class.

Accessing the Assertion instance

Leo holds a single reference to an instance of Assertion. This ensures that all plugins are extending the same thing.

<?php
use Peridot\Leo\Leo;

$assertion = Leo::assertion();
?>

Adding methods

Methods can be added and overwritten using the ->addMethod method of the Assertion class.

<?php
$assertion->addMethod('hello', function ($name) {
    print "hello $name";
});
?>

Methods added this way can then be accessed on the assertion instance.

<?php
$assertion->hello('everybody');
?>

Adding properties

Part of Leo's power is it's ability to build expressive language chains. The ->addProperty method can be used to register properties that are lazily evaluated.

<?php
$assertion->addProperty('dom', function() use ($driver) {
    $dom = $driver->get($this->getActual());
    $this->flag('dom', $dom)
    return $this;
});
?>

The function used to create the property will be evaluated when the property is accessed.

<?php
expect('http://app.com')->dom->to->have->element('.some-css-class');
?>

Memoization

If you know your property is going to always return the same result, you can cache that result by setting the memoize argument to true when adding a property.

<?php
$assertion->addProperty('db', function () {
    //some expensive creation operation
    $db = new EntityManager();
    //setup schema? load mappings?
    return $db;
}, true); //memoize set to true, so the db is cached and the same instance is always returned 

expect('table')->in->db->to->have->record('id'); //db is cached so it wont have to be created multiple times
?>

Flags

Flags can be used to set arbitrary state on the assertion. They are useful for carrying state through an assertion chain, and can be useful for changing behavior based on some condition.

This is exactly how assertions are able to convey a negated state via the not language chain.

<?php
$assertion->addProperty('not', function () {
    return $this->flag('not', true);
});
?>

Using Assertion::extend

As a convenience, the Assertion class contains an extend method that is useful for leveraging packaged plugins. It accepts either a callable or the path to a file that returns a callable.

<?php
$assertion->extend(function ($assertion) {
    //add methods and properties
});

$assertion->extend(new InvokableObjectWithPluginThings());

//plugin.php
return function($assertion) {
    //add methods and properties
};

// peridot.php/bootstrap.php
Leo::assertion()->extend('plugin.php');
?>

Matchers

Most assertion methods in Leo are implemented by leveraging matchers - that is classes that implement MatcherInterface.

Matchers are the classes used to determine if an actual value matches some criteria, and they are also responsible for defining templates for failure messages.

When a method added via Assertion::addMethod returns a matcher, Leo will automatically perform an assertion using that matcher when the method is called:

<?php
$assertion->addMethod('equal', function ($expected, $message = "") {
    $this->flag('message', $message);
    return new EqualMatcher($expected);
});

//assertion will be performed against EqualMatcher
$assertion->equal(1, 2);
?>

Most matchers only have to implement two methods when extending the AbstractMatcher. Here is the code behind the core EqualMatcher.

<?php
namespace Peridot\Leo\Matcher;

use Peridot\Leo\Matcher\Template\ArrayTemplate;
use Peridot\Leo\Matcher\Template\TemplateInterface;

class EqualMatcher extends AbstractMatcher
{
    public function doMatch($actual)
    {
        return $this->expected == $actual;
    }

    public function getDefaultTemplate()
    {
        return new ArrayTemplate([
            'default' => 'Expected {{expected}}, got {{actual}}',
            'negated' => 'Expected {{expected}} not to equal {{actual}}'
        ]);
    }
}
?>

You can browse all the core matchers on GitHub.

Templates

The ArrayTemplate is an instance of TemplateInterface. Templates are used by matchers to provide default and negated failure messages for when a match fails. Matchers leverage templates via their getTemplate, setTemplate, and getDefaultTemplate methods.

Formatters

Formatters use match results together with templates to return human readable strings for the results of a match. All formatters should implement FormatterInterface. For most cases, the Formatter that ships with Leo should suffice.

Responders

Responders are the last stop for a Leo assertion. Responders are in charge of doing something with the formatted results of a match. All responders should implement ResponderInterface. The default ExceptionResponder responds by throwing an exception with the formatted message.

<?php
namespace Peridot\Leo\Responder;

use Exception;
use Peridot\Leo\Formatter\FormatterInterface;
use Peridot\Leo\Matcher\Match;
use Peridot\Leo\Matcher\Template\TemplateInterface;

class ExceptionResponder implements ResponderInterface
{    
    protected $formatter;

    public function __construct(FormatterInterface $formatter)
    {
        $this->formatter = $formatter;
    }

    public function respond(Match $match, TemplateInterface $template, $message = "")
    {
        if ($match->isMatch()) {
            return;
        }

        $this->formatter->setMatch($match);
        $message = ($message) ? $message : $this->formatter->getMessage($template);
        throw new Exception($message);
    }
}
?>

Using your own formatters and responders

You can replace the default formatter and responder via the static methods Leo::setFormatter and Leo::setResponder.

Example

LeoHttpFoundation is a simple plugin that extends Leo to simplify testing HttpFoundation based applications. Browse the source for some examples on extending Leo.