How to fix: You have requested a non-existent service “test.client” with Behat 3

So, it’s been a long day of development.

And the last 50 minutes have been particularly painful.

I’ve been bringing FOSUserBundle, Dunglas API Bundle, Lexik JWT Bundle, and Behat (amongst a good few others) together into one project for the first time.

Most of today has been productive. I’ve solved two of my big headaches, but as the hours have gone by, tiredness has set in.

And rather than call it a night, I did my usual “I’ll just see if I can…” once too many times, and buggered everything up.

If I had been behaving, I could have quickly done a little git bisect magic and found the source of my woes. Alas, I had not been behaving.

Anyway, the issue:

Run bin/behat , encounter error:

  [Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException]
  You have requested a non-existent service "test.client".

Wot do?

Well, there is a major shortage of Google help on this one.

The two most likely answers were not it.

Behat had been working just fine. I knew it was my mistake. But the project is growing and there’s just a ton of possible files that could have changed. When will I learn?

As it turned out, I had changed my behat.yml file in my tired stupor:

# behat.yml 
 extensions:
    Behat\Symfony2Extension:
      kernel:
        env: "test"
        debug: "true"

I’d changed env to a new Symfony Environment I had created so as not to keep messing up my local dev database every time I re-ran behat.

In my case changing back to env: “test” solved all my problems.

Ok, well not all of them… plenty of work still to do. But that’s it for this evening.

I hope that saves someone a headache in the future.

Time to commit and go to bed.

Renaming Routes in DunglasApiBundle

Let’s say you have defined a new Resource  entry for your SocialMediaAccount  Entity:

# app/config/services.yml

    resource.social_media_account:
        parent:    "api.resource"
        arguments: [ "AppBundle\\Entity\\SocialMediaAccount" ]
        tags:      [ { name: "api.resource" } ]

Now, when we run php app/console debug:router we see the following:

dunglas-api-bundle-rename-routes

Ack. Underscores in our URLs.

Most likely not what we want.

Thankfully, changing this is really easy:

# app/config/services.yml

    resource.social_media_account:
        parent:    "api.resource"
        arguments: [ "AppBundle\\Entity\\SocialMediaAccount" ]
        calls:
            - method: "initShortName"
              arguments: [ 'social-media-account' ]
        tags:      [ { name: "api.resource" } ]

Success:

dunglasapibundle-custom-route-names

But, how did I know to add in that specific method in the calls block?

This line: parent: “api.resource” tells us that we are using a Parent Service.

Parent services are something of a trade off.

They decrease the amount of configuration you are required to write, but increase the technical complexity of comprehending the configuration.

The idea is that if we have two or more classes that are being configured as Symfony Services, and that they share identical setup requirements, then we can extract the common / shared requirements to a parent class, and tell our child services to use the configuration from the parent.

We can now remove the duplicated service setup / configuration.

However, newer or less experienced developers who read our code / configuration will have a harder time understanding what is happening.

But back to DunglasApiBundle!

We are using api.resource as our Parent service. This service definition must live somewhere, and as Symfony Bundles tend to follow a common structure, the best place to start looking would be in:

/vendor/dunglas/api-bundle/Resources/config/

There’s a fair few files in here:

dunglas-api-bundle-resources

If unsure, do a quick search for ‘api.resources’ in that folder, or resort to the old school method of opening them one-by-one and eye balling the service definitions until you find the one you want.

In our case, it lives inside api.xml , nice and easy:

<service id="api.resource" class="Dunglas\ApiBundle\Api\Resource" public="false" abstract="true" lazy="true" />

Now we have the class name so we know exactly where to look.

With the file found (/vendor/dunglas/api-bundle/Api/Resource.php ), all we need to know are the available method names. Thankfully, PHPStorm can provide us with a nice list of them:

dunglas-api-bundle-resource-php-methods

Sorry about the truncating. If you want to see that view then hit cmd / ctrl + 7, and then cmd / ctrl + 1 to return to ‘normal’ when you are done.

Now we have our list of methods, we can simply add the required methods, along with their parameters to our calls block in our concrete Resource setup:

# app/config/services.yml

    resource.social_media_account:
        parent:    "api.resource"
        arguments: [ "AppBundle\\Entity\\SocialMediaAccount" ]
        calls:
            - method: "initShortName"
              arguments: [ 'social-media-account' ]
        tags:      [ { name: "api.resource" } ]

Be sure to read up on Symfony Parent Services to understand this further.

Happy Dunglas’ing! 🙂

#PHPNW15Nelly Visits the USA

phpnw15 side questBack at the start of October (time flies) I spent the weekend at PHPNW15.

One of the available side quests was to send the PHPNW nellyphant off to unusual locations, in order to win one of 8 licenses for PHP Storm.

Being quite the fan of PHPStorm, the challenge was dutifully accepted.

But what exciting and unusual places could I get Nelly of to within the time frame?

As it happens (and with the help of a very generous relative), I managed to get my Nelly about 4200 miles away!

Amazingly, with America’s ultra-strict airport security, Nelly made it through without having to provide finger foot prints or retina scans.

#PHPNW15Nelly At The Magic Kingdom

php nelly at the magic kingdom
Nelly visits the Disney Princess Castle at Magic Kingdom
php-elephant-at-tomorrow-land
Nelly journeys to Tomorrow Land, where PHP7 is already an actual thing (probably)

#PHPNW15Nelly Visits NASA

php-elephant-atlantis-shuttle
Nelly harbours ambitions of going into orbit
php-elephant-hitches-a-ride-on-atlantis
PHPNelly says “Auf Wiedersehen”, and that was the last time we saw her

If only I could have smuggled myself in to their suitcase.

Mocking Collections in PHPSpec

phpspec-logoI’m a huge fan of PHPSpec, and its close cousin, Behat. I find when writing code in conjunction with PHPSpec, I am able to enter a rhythm that I have never found with any other tool.

I particularly enjoy the code generation functionality – describe some action, do a bin/phpspec run and have your methods created for you as you go. It really is quite a joy to use.

However, as with any tool, there is a learning curve.

I found the basics – the stuff described in the manual – to be straightforward enough that even when stuck, I could relatively quickly find my way through and get back on track.

Then, recently, I decided to build an application involving third party / social media providers for authentication using HWIOAuthBundle.

Along the way, I added the concept of a User object having a Collection (Doctrine\Common\Collections\Collection) of Account objects. Fairly common stuff, particularly if you have ever used Symfony at all.

The very basic idea would be something like this:

<?php

namespace AppBundle\OAuth\Connect;

use HWI\Bundle\OAuthBundle\OAuth\Response\UserResponseInterface;
use HWI\Bundle\OAuthBundle\Security\Core\User\FOSUBUserProvider as BaseClass;
use FOS\UserBundle\Model\UserManagerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Core\User\UserInterface;

class ProfileConnector extends BaseClass
{
    /**
     * @var RequestStack
     */
    private $requestStack;

    public function injectRequestStack(RequestStack $requestStack)
    {
        $this->requestStack = $requestStack;
    }

    public function connect(UserInterface $user, UserResponseInterface $response)
    {
        $req = $this->requestStack->getMasterRequest();

        if ( ! $req->request->has('profiles')) {
            return false;
        }

        $selectedProfiles = $req->request->get('profiles');

        /** @var $user \AppBundle\Entity\User */
        /** @var $profile \AppBundle\Model\ProfileInterface */
        foreach ($user->getProfiles() as $profile) {
            if ( ! array_key_exists($profile->getId(), $selectedProfiles)) {
                continue;
            }
        }

        // something else here
        return true;
    }
}

 

This is a work in progress, so if it looks a little rough… hey, that’s why I do TDD. The refactoring will come in time.

Seeing the tests would likely also help:

<?php

namespace spec\AppBundle\OAuth\Connect;

use Doctrine\Common\Collections\ArrayCollection;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use HWI\Bundle\OAuthBundle\OAuth\Response\UserResponseInterface;
use FOS\UserBundle\Model\UserManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use AppBundle\Model\ProfileInterface;
use AppBundle\Entity\Profile;
use AppBundle\Entity\User;
use AppBundle\Entity\SocialMediaAccount;

class ProfileConnectorSpec extends ObjectBehavior
{
    private $user;
    private $response;

    function let(UserManagerInterface $userManager, User $user, UserResponseInterface $userResponse)
    {
        $this->user = $user;
        $this->response = $userResponse;

        $this->beConstructedWith($userManager, []);
    }

    function it_is_initializable()
    {
        $this->shouldHaveType('AppBundle\OAuth\Connect\ProfileConnector');
        $this->beAnInstanceOf('HWI\Bundle\OAuthBundle\Security\Core\User\FOSUBUserProvider');
    }

    function it_can_inject_the_request_stack()
    {
        $this->injectRequestStack(new RequestStack());
    }

    function it_can_handle_no_profile_accounts_being_selected()
    {
        $requestStack = new RequestStack();
        $requestStack->push(new Request());

        $this->injectRequestStack($requestStack);

        $this->connect($this->user, $this->response)->shouldReturn(false);
    }

    function it_can_connect_one_account_to_a_social_media_service(User $user, Profile $profile)
    {
        $profile->getId()->willReturn(16);
        $profiles = new ArrayCollection([$profile->getWrappedObject()]);

        $user->getProfiles()->willReturn($profiles);

        $requestStack = new RequestStack();
        $requestStack->push(new Request([], ['profiles'=>[16=>'on']]));

        $this->injectRequestStack($requestStack);

        $this->connect($user, $this->response)->shouldReturn(true);
    }

    function it_can_connect_multiple_accounts_to_a_social_media_service()
    {

    }
}

This is absolutely a work in progress. I even left in the last test, yet to be started.

As a quick side note, for the first time I have decided to declare commonly required objects by way of the let() method. I am not absolutely sure whether or not this is good practice, but it does seem to work as I intended it too. If you know differently, do let me know by way of leaving a comment – thanks !

Currently the tests are passing – with the exception of the pending example still to write.

What may be less obvious is how much effort I went through to get the it_can_connect_one_account_to_a_social_media_service() example to play ball.

I’ve spent a good few hours these past few evenings trying to figure out this error:

AppBundle/OAuth/Connect/ProfileConnector
  51  - it can connect one account to a social media service
      warning: array_key_exists(): The first argument should be either a string or an integer in
      /var/www/myproj/src/AppBundle/OAuth/Connect/ProfileConnector.php line 46

When I was seeing this error, the problematic test actually looked like this:

function it_can_connect_one_account_to_a_social_media_service(User $user, Profile $profile)
{
    $profile->getId()->willReturn(16);
    $user->addProfile($profile);

    $requestStack = new RequestStack();
    $requestStack->push(new Request([], ['profiles'=>[16=>'on']]));

    $this->injectRequestStack($requestStack);

    $this->connect($user, $this->response)->shouldReturn(true);
}

A bit of Google-foo told me I was likely going about this entirely the wrong way:

https://codereviewvideos.com/blog/wp-content/uploads/2015/10/everzet-stop-mocking-collections.png

When the creator of PHPSpec tells you (indirectly) that you are wrong, then… you are wrong.

I could actually see the problem – the implementation seemed correct, but PHPSpec sees my call $profile->getId() as returning an Object, and then throwing an error something along the lines of :

AppBundle/OAuth/Connect/ProfileConnector
  51  - it can connect one account to a social media service
      error: Object of class Prophecy\Prophecy\MethodProphecy could not be converted to string in
      /var/www/myproj/src/AppBundle/OAuth/Connect/ProfileConnector.php line 44

At first I figured… ok, well I won’t mock the User object – I can new that up – but when trying to add a mock Profile to the real collection, it all went a bit wrong:

AppBundle/OAuth/Connect/ProfileConnector
  50  - it can connect one account to a social media service
      error: Argument 1 passed to AppBundle\Entity\User::addProfile() must implement interface
      AppBundle\Model\ProfileInterface, instance of PhpSpec\Wrapper\Collaborator given, called in
      /var/www/myproj/spec/AppBundle/OAuth/Connect/ProfileConnectorSpec.php on line 54 and defined in
      /var/www/myproj/src/AppBundle/Entity/User.php line 44

That’s fine, I thought, I will use a real Profile object as well, because why not?

Well, I’ll tell you why not.

The test expects my Profile object to have an id of 16. I’m not about to add in a setId() method, that would be bonkers.

At heart, I knew as soon as I started adding in real objects that I was heading down the wrong path.

Generally I find that when PHPSpec is making your life hard it is because you are trying to do the wrong thing. Sooner or later you must stop resisting.

Star Trek: The Next Generation 365 (Star Trek 365)
Picard lost most of his hair due to frustrating late night bug fixing in ten forward. It’s all explained in the Season 3 episode Bynar2Hex.

Anyway, after quite a bit a lot of further hackery, I managed to find a working solution (as per the earlier sample):

    function it_can_connect_one_account_to_a_social_media_service(User $user, Profile $profile)
    {
        $profile->getId()->willReturn(16);
        $profiles = new ArrayCollection([$profile->getWrappedObject()]);

        $user->getProfiles()->willReturn($profiles);

        $requestStack = new RequestStack();
        $requestStack->push(new Request([], ['profiles'=>[16=>'on']]));

        $this->injectRequestStack($requestStack);

        $this->connect($user, $this->response)->shouldReturn(true);
    }

When I think about it now, it does make sense. This is similar to what I was trying to do, only this way conforms with the way PHPSpec expects me to behave 🙂

Both the User and Profile objects are properly mocked, but as per Everzet’s comment, we are returning a real ArrayCollection, which by way of $profile->getWrappedObject() will return the underlying Profile objects, rather than the PHPSpec wrapped objects / Object Prophecies.

This is exactly the sort of problem that my brother – an aspiring coder – would think I “just knew” how to fix. And that he should also be expected to know how to solve instinctively also.

Of course, the next time this comes up, I will know exactly how to solve it. It’s just if you were sat watching over my shoulder, you wouldn’t imagine the hours of my life that lesson took to learn 😉