phpphpspec

Custom PhpSpec matcher and/or extension not working


I'm trying to test if a class is final. Since I've not found a default matcher for this (or any other clean way of testing it), I decided to create a custom extension which adds a new matcher to do just that, but I can't get it to work.

I've tried it with an inline matcher, like so:

public function getMatchers(): array
{
    return [
        'beFinal' => function ($subject) {
            $reflection = new \ReflectionClass($subject);

            if (!$reflection->isFinal()) {
                throw new FailureException('Expected subject to be final, but it is not.');
            }
            return true;
        },
    ];
}

This works well enough when I call $this->shouldBeFinal();. The problem is that when I call $this->shouldNotBeFinal();, it outputs a generic message: [obj:Class\To\Test] not expected to beFinal(), but it did., instead of one I'd like to show.

Another problem is that I don't want this for just one class. That's why I decided to make an extension for it.

Here's what I got:

phpspec.yml:

extensions:
    PhpSpecMatchers\Extension: ~

PhpSpecMatchers/Extension.php:

<?php

declare(strict_types=1);

namespace PhpSpecMatchers;

use PhpSpec\ServiceContainer;
use PhpSpecMatchers\Matchers\BeFinalMatcher;

class Extension implements \PhpSpec\Extension
{
    public function load(ServiceContainer $container, array $params): void
    {
        $container->define(
            'php_spec_matchers.matchers.be_final',
            function ($c) {
                return new BeFinalMatcher();
            },
            ['matchers']
        );
    }
}

PhpSpecMatchers/Matchers/BeFinalMatcher.php:

<?php

declare(strict_types=1);

namespace PhpSpecMatchers\Matchers;

use PhpSpec\Exception\Example\FailureException;
use PhpSpec\Matcher\BasicMatcher;

class BeFinalMatcher extends BasicMatcher
{
    public function supports(string $name, $subject, array $arguments): bool
    {
        return $name === 'beFinal';
    }

    public function getPriority(): int
    {
        return 0;
    }

    protected function matches($subject, array $arguments): bool
    {
        $reflection = new \ReflectionClass($subject);

        return $reflection->isFinal();
    }

    protected function getFailureException(string $name, $subject, array $arguments): FailureException
    {
        return new FailureException('Expected subject to not be final, but it is.');
    }

    protected function getNegativeFailureException(string $name, $subject, array $arguments): FailureException
    {
        return new FailureException('Expected subject to be final, but it is not.');
    }
}

Whenever I try to call $this->beFinal(); with this configuration, the spec is broken and shows the following message: method [array:2] not found.. If I add an isFinal() method to the class I'm testing and return true for example, it passes for $this->shouldBeFinal(); and fails for $this->shouldNotBeFinal();, but I don't want to add that method. I should just work without it and for as far as I understand it should be able to work like that, right?

I've also tried adding custom suites to my phpspec.yml, like so:

suites:
    matchers:
        namespace: PhpSpecMatchers\Matchers
        psr4_prefix: PhpSpecMatchers\Matchers
        src_path: src/PhpSpecMatchers/Matchers
        spec_prefix: spec/PhpSpecMathcers/Matchers

But that doesn't change anything. I've also tried to add the following config to phpspec.yml:

extensions:
    PhpSpecMatchers\Extension:
        php_spec_matchers:
        src_path: src
        spec_path: spec

That also doesn't change anything.

One other thing I've tried was to ditch the extension approach and just declare my mather in phpspec.yml, like so:

matchers:
    - PhpSpecMatchers\Matchers\BeFinalMatcher

As you might expect: same results.

The loading in PhpSpecMatchers\Extension does get called (tested by a simple var_dump(…);), but it doesn't seem to reach anything within PhpSpecMatchers\Matchers\BeFinalMatcher, since I don't get any output from any var_dump(…);

I've followed tutorials and examples from symfonycasts, phpspec docs itself and some other github project I found, they're all almost identically to my code (except namespaces, directory structures and stuff like that), so I'm kind of at a loss here.

How do I get it to a point where I can successfully call $this->shouldBeFinal(); and $this->shouldNotBeFinal();?

Many thanks to whovere can help me out here.

P.S.: I've also posted this issue on phpspec's github.


Solution

  • So apparently the priority was too low (see this comment my phpspec's github issue). PhpSpec\Matcher\IdentityMatcher (where the shouldBe comes from) extends from PhpSpec\Matcher\BasicMatcher where the priority is set to 100. Since mine was set to 0 it got to mine first (I think) and therefore didn't execute properly. I've set my priority to 101 and it works flawlessly (except I had switched the positive and negative messages, I found out).