Showing posts with label Frameworks. Show all posts
Showing posts with label Frameworks. Show all posts

Thursday 4 March 2021

Kahlan: getting it working with Symfony 5 and generating a code coverage report

G'day:

This is not the article I intended to write today. Today (well: I hoped to have it done by yesterday, actually) I had hoped to be writing about my happy times doing using TDD to implement a coupla end-points I need in my Symfony-driven web service. I got 404 (chuckle) words into that and then was blocked by trying to get Kahlan to play nice for about five hours (I did have dinner in that time too, but it was add my desk, and with a scowl on my face googling stuff). And that put me at 1am so I decided to go to bed. I resumed today an hour or so ago, and am just now in the position to get going again. But I've decided to document that lost six hours first.

I sat down to create a simple endpoint to fetch some data, and started by deciding on my first test case, which was "It needs to return a 200-OK status for GET requests on the /workshops endpoint". I knew Symfony did some odd shenanigans to be able to functionally test right from a route slug rather than having tests directly instantiating controller classes and such. I checked the docs and all this is done via an extension of PHPUnit, using WebTestCase. But I don't wanna use PHPUnit for this. Can you imagine my test case name? Something like: testItNeedsToReturnA200OKStatusForGetRequestsOnTheWorkshopsEndpoint. Or I could break PSR-12/PSR-1 and make it (worse) with test_it_needs_to_Return_a_200_OK_status_for_get_requests_on_the_workshops_endpoint (this is why I will never use phpspec). Screw that. I'm gonna work out how to do these Symfony WebTestCase tests in Kahlan.

Not so fast mocking PHPUNit there, Cameron

2021-03-06

Due to some - probably show-stopping - issues I'm seeing with Kahlan, I have been looking at PHPUnit some more. I just discovered the textdox reporting functionality it has, which makes giving test case names much clearer.

/** @testdox Tests the /workshops endpoint methods */
class workshopsEndPointTet {
    /** @testdox it needs to return a 200-OK status for GET requests */
    function testReturnStatus() {}
}

This will output in the test runs as:

Perfect. I still prefer the style of code Kahlan uses, but… this is really good to know.

First things first, I rely heavily on PHPUnit's code coverage analysis, so I wanted to check out Kahan's offering. The docs seem pretty clear ("Code Coverage"), and seems I just want to be using the lcov integration Kahlan offers, like this:

vendor/bin/kahlan --lcov="var/tmp/lcov/coverage.info"
genhtml --output-directory public/lcov/ var/tmp/lcov/coverage.info

I need to install lcov first via my Dockerfile:

RUN apt-get install lcov --yes

OK so I did all that, and had a look at the report:

Pretty terrible coverage, but it's working. Excellent. But drilling down into the report I see this:

>

This is legit reporting because I have no tests for the Kernel class, but equally that class is generated by Symfony and I don't want to cover that. How do I exclude it from code coverage analysis? I'm looking for Kahlan's equivalent of PHPUnit's @codeCoverageIgnore. There's nothing in the docs, and all I found was a passing comment against an issue in Github asking the same question I was: "Exclude a folder in code coverage #321". The answer is to do this sort of thing to my kahlan-config.php file:

use Kahlan\Filter\Filters;
use Kahlan\Reporter\Coverage;
use Kahlan\Reporter\Coverage\Driver\Xdebug;

$commandLine = $this->commandLine();
$commandLine->option('no-header', 'default', 1);

Filters::apply($this, 'coverage', function($next) {
    if (!extension_loaded('xdebug')) {
        return;
    }
    $reporters = $this->reporters();
    $coverage = new Coverage([
        'verbosity' => $this->commandLine()->get('coverage'),
        'driver'    => new Xdebug(),
        'path'      => $this->commandLine()->get('src'),
        'exclude'   => [
            'src/Kernel.php'
        ],
        'colors'    => !$this->commandLine()->get('no-colors')
    ]);
    $reporters->add('coverage', $coverage);
});

That seems a lot of messing around to do something that seems like it should be very simple to me. I will also note that Kahlan - currently - has no ability to suppress code coverage at a method or code-block level either (see "Skip individual functions in code coverage? #333"). This is not a deal breaker for me in this work, but it would be a show-stopper on any of the codebases I have worked on in the last decade, as they've all been of dubious quality, and all needed some stuff to be actively "overlooked" as they're not testable as they currently stand, and we (/) like my baseline code coverage report to have 100% coverage reported, and be entirely green. This is so if any omissions creep in, they're easy to spot (see "Yeah, you do want 100% test coverage"). Anyway, I'll make that change and omit Kernel from analysis:

root@13038aa90234:/usr/share/fullstackExercise# vendor/bin/kahlan --lcov="var/tmp/lcov/coverage.info"

.................                                                 17 / 17 (100%)



Expectations   : 51 Executed
Specifications : 0 Pending, 0 Excluded, 0 Skipped

Passed 17 of 17 PASS in 0.527 seconds (using 7MB)

Coverage Summary
----------------

Total: 33.33% (1/3)

Coverage collected in 0.001 seconds


root@13038aa90234:/usr/share/fullstackExercise# genhtml --output-directory public/lcov/ var/tmp/lcov/coverage.info
Reading data file var/tmp/lcov/coverage.info
Found 2 entries.
Found common filename prefix "/usr/share/fullstackExercise"
Writing .css and .png files.
Generating output.
Processing file src/MyClass.php
Processing file src/Controller/GreetingsController.php
Writing directory view page.
Overall coverage rate:
  lines......: 33.3% (1 of 3 lines)
  functions..: 50.0% (1 of 2 functions)
root@13038aa90234:/usr/share/fullstackExercise#

And the report now doesn't mention Kernel:

Cool.

Now to implement that test case. I need to work out how to run a Symfony request without using WebTestCase. Well I say "I need to…" I mean I need to google someone else who's already done it, and copy them. I have NFI how to do it, and I'm not prepared to dive into Symfony code to find out how. Fortunately someone has already cracked this one: "Functional Test Symfony 4 with Kahlan 4". It says "Symfony 4", but I'll check if it works on Symfony 5 too. I also happened back to the Kahlan docs, and they mention the same guy's solution ("Integration with popular frameworks › Symfony"). This one points to a library to encapsulate it (elephantly/kahlan-bundle), but that is actively version-constrained to only Symfony 4. Plus it's not seen any work since 2017, so I suspect it's abandoned.

Anyway, back to samsonasik's blog article. It looks like this is the key bit:

$this->request = Request::createFromGlobals();
$this->kernel  = new Kernel('test', false);
$request = $this->request->create('/lucky/number', 'GET');
$response = $this->kernel->handle($request);

That's how to create a Request and get Symfony's Kernel to run it. Easy. Hopefully. Let's try it.

namespace adamCameron\fullStackExercise\spec\functional\Controller;

use adamCameron\fullStackExercise\Kernel;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

describe('Tests of GreetingsController', function () {

    beforeAll(function () {
        $this->request = Request::createFromGlobals();
        $this->kernel  = new Kernel('test', false);
    });

    describe('Tests of doGet', function () {
        it('returns a JSON greeting object', function () {
            $testName = 'Zachary';

            $request = $this->request->create("/greetings/$testName", 'GET');
            $response = $this->kernel->handle($request);

            expect($response->getStatusCode())->toBe(Response::HTTP_OK);
        });
    });
});

I'm not getting too ambitious here, and it's not addressing the entire test case yet. I'm just making the request and checking its response status code.

And this just goes splat:

root@13038aa90234:/usr/share/fullstackExercise# vendor/bin/kahlan --lcov="var/tmp/lcov/coverage.info" --ff

E                                                                 18 / 18 (100%)


Tests of GreetingsController
  Tests of doGet
    ✖ it returns a JSON greeting object
      an uncaught exception has been thrown in `vendor/symfony/framework-bundle/Kernel/MicroKernelTrait.php` line 91

      message:`Kahlan\PhpErrorException` Code(0) with message "`E_WARNING` require(/tmp/kahlan/usr/share/fullstackExercise/src/config/bundles.php): Failed to open stream: No such file or directory"

        [NA] - vendor/symfony/framework-bundle/Kernel/MicroKernelTrait.php, line  to 91

Eek. I had a look into this, and the code in question is try to do this:

$contents = require $this->getProjectDir().'/config/bundles.php';

And the code in getProjectDir is thus:

<pre class="source-code"><code>public function getProjectDir()
{
    if (null === $this-&gt;projectDir) {
        $r = new \ReflectionObject($this);

        if (!is_file($dir = $r-&gt;getFileName())) {
            throw new \LogicException(sprintf('Cannot auto-detect project dir for kernel of class &quot;%s&quot;.', $r-&gt;name));
        }

        $dir = $rootDir = \dirname($dir);
        while (!is_file($dir.'/composer.json')) {
            if ($dir === \dirname($dir)) {
                return $this-&gt;projectDir = $rootDir;
            }
            $dir = \dirname($dir);
        }
        $this-&gt;projectDir = $dir;
    }

    return $this-&gt;projectDir;
}
</code></pre>

The code starts in the directory of the current file, and traverses up the directory structure until it finds the directory with composer.json.If it doesn't find that, then - somewhat enigmatically, IMO - it just says "ah now, we'll just use the directory we're in now. It'll be grand". To me if it expects to find what it's looking for by looking up the ancestor directory path and that doesn't work: throw an exception. Still. In the normal scheme of things, this would work cos the app's Kernel class - by default - seems to live in the src directory, which is one down from where composer.json is.

So why didn't it work? Look at the directory that it's trying to load the bundles from: /tmp/kahlan/usr/share/fullstackExercise/src/config/bundles.php. Where? /tmp/kahlan/usr/share/fullstackExercise/. Ain't no app code in there, pal. It's in /usr/share/fullstackExercise/. Why's it looking in there? Because the Kernel object that is initiating all this is at /tmp/kahlan/usr/share/fullstackExercise/src/Kernel.php. It's not the app's own one (at /usr/share/fullstackExercise/src/Kernel.php), it's all down to how Kahlan monkey-patches everything that's going to be called by the test code, on the off chance you want to spy on anything. It achieves this by loading the source code of the classes, patching the hell out of it, and saving it in that /tmp/kahlan. The only problem with this is that when Symfony traverses up from where the patched Kernel class is… it never finds composer.json, so it just takes a guess at where the project directory is. And it's not a well-informed guess.

I'm not sure who I blame more here, to be honest. Kahlan for patching everything and running code from a different directory from where it's supposed to be; or Symfony for its "interesting" way to know where the project directory is. I have an idea here, Symfony: you could just ask me. Or even force me tell it. Ah well. Just trying to be "helpful" I s'pose.

Anyway, I can exclude files from being patched, according to the docs:

  --exclude=<string>                  Paths to exclude from patching. (default: `[]`).

I tried sticking the path to Kernel in there: src/Kernel.php, and that didn't work. I hacked about in the code and it doesn't actually want a path, it wants the fully-qualified class name, eg: adamCameron\fullStackExercise\Kernel. I've raised a ticket for this with Kahlan, just to clarify the wording there: Bug: bad nomenclature in help: "path" != "namespace".

This does work…

root@13038aa90234:/usr/share/fullstackExercise# vendor/bin/kahlan --lcov="var/tmp/lcov/coverage.info" --ff --exclude=adamCameron\\fullStackExercise\\Kernel

E                                                                 18 / 18 (100%)


Tests of GreetingsController
  Tests of doGet
    ✖ it returns a JSON greeting object
      an uncaught exception has been thrown in `vendor/symfony/deprecation-contracts/function.php` line 25

      message:`Kahlan\PhpErrorException` Code(0) with message "`E_USER_DEPRECATED` Please install the \"intl\" PHP extension for best performance."

        [NA] - vendor/symfony/deprecation-contracts/function.php, line  to 25
        trigger_deprecation - vendor/symfony/framework-bundle/DependencyInjection/FrameworkExtension.php, line  to 253

This is not exactly what I want, but it's a different error, so Symfony is finding itself this time, and then just faceplanting again. However when I look into the code, it's this:

if (!\extension_loaded('intl') && !\defined('PHPUNIT_COMPOSER_INSTALL')) {
    trigger_deprecation('', '', 'Please install the "intl" PHP extension for best performance.');
}

// which in turn...

function trigger_deprecation(string $package, string $version, string $message, ...$args): void
{
    @trigger_error(($package || $version ? "Since $package $version: " : '').($args ? vsprintf($message, $args) : $message), \E_USER_DEPRECATED);
}

So Symfony is very quietly raising a flag that it suggests I have that extension installed. But only as a deprecation notice, and even then it's @-ed out. Somehow Kahlan is getting hold of that and going "nonono, this is worth stopping for". No it ain't. Ticket raised: "Q: should trigger_error type E_USER_DEPRECATED cause testing to halt?".

Anyway, the point is a legit one, so I'll install the intl extension. I initially thought it was just a matter of slinging this in the Dockerfile:

RUN apt-get install --yes zlib1g-dev libicu-dev g++
RUN docker-php-ext-install intl

But that didn't work, I needed a bunch of Other Stuff too:

RUN apt-get install --yes zlib1g-dev libicu-dev g++
RUN docker-php-ext-install pdo_mysql
RUN docker-php-ext-configure intl
RUN docker-php-ext-install intl

(Thanks to the note in docker-php-ext-install intl fails #57 for solving that for me).

After rebuilding the container, let's see what goes wrong next:

root@58e3325d1a16:/usr/share/fullstackExercise# composer coverage
> vendor/bin/kahlan --lcov="var/tmp/lcov/coverage.info" --exclude=adamCameron\\fullStackExercise\\Kernel

..................                                                18 / 18 (100%)



Expectations   : 52 Executed
Specifications : 0 Pending, 0 Excluded, 0 Skipped

Passed 18 of 18 PASS in 0.605 seconds (using 12MB)

Coverage Summary
----------------

Total: 100.00% (3/3)

Coverage collected in 0.001 seconds


> genhtml --output-directory public/lcov/ var/tmp/lcov/coverage.info
Reading data file var/tmp/lcov/coverage.info
Found 2 entries.
Found common filename prefix "/usr/share/fullstackExercise"
Writing .css and .png files.
Generating output.
Processing file src/MyClass.php
Processing file src/Controller/GreetingsController.php
Writing directory view page.
Overall coverage rate:
  lines......: 100.0% (3 of 3 lines)
  functions..: 100.0% (2 of 2 functions)
root@58e3325d1a16:/usr/share/fullstackExercise#

(I've stuck a Composer script in for this, btw):

"coverage": [
    "vendor/bin/kahlan --lcov=\"var/tmp/lcov/coverage.info\" --exclude=adamCameron\\\\fullStackExercise\\\\Kernel",
    "genhtml --output-directory public/lcov/ var/tmp/lcov/coverage.info"
]

And most promising of all is this:

All green! I like that.

And now I'm back to where I wanted to be, yesterday, as I typed that 404th word of the article I was meant to be working on. 24h later now.

Righto.

--
Adam

Thursday 14 January 2021

Part 7: Using Symfony

G'day:

Familiar boilterplate about how this is one in a series follows. Please note that I initially intended this to be a single article, but by the time I had finished the first two sections, it was way too long for a single read, so I've split it into the following sections, each as their own article:

  1. Intro / Nginx
  2. PHP
  3. PHPUnit
  4. Tweaks I made to my Bash environment in my Docker containers
  5. MariaDB
  6. Installing Symfony
  7. Using Symfony (this article)
  8. Testing a simple web page built with Vue.js using Mocha, Chai and Puppeteer
  9. I mess up how I configure my Docker containers
  10. An article about moving files and changing configuration
  11. Setting up a Vue.js project and integrating some existing code into it
  12. Unit testing Vue.js components

As indicated: this is the seventh article in the series, and follows on from Part 6: Installing Symfony. It's probably best to go have a breeze through the earlier articles first, in particular the immediately preceding one. Also as indicated in earlier articles: I'm a noob with all this stuff so this is basically a log of me working out how to get things working, rather than any sort of informed tutorial on the subject. Also I was only originally "planning" one article on getting the Symfony stuff sorted out, but yesterday's exercise was more involved that I'd've liked, so I stopped once I had Symfony serving up its welcome page, and today focusing on the config / code of getting a route, controller, model, view (actually there's no model or view in this endpoint; it's all just done in the controller) up and running.

Two caveats before I start.

Firstly: this is the first time I've done anything with Symfony other than reviewing the work of other members of my team - the ones doing the actual work. I have some familiarity with Symfony, but it's very very superficial.

Secondly, I will also be up front that I don't like Symfony's approach to MVC frameworking. Symfony is kind of a "lifestyle choice": it's opinionated, it expects you to do things a certain way, and it puts its nose into everything. This is opposed to something like Silex which simply provides the bare bones wiring to handle the ubiquitous web application requirement of guiding processing through routing, controller, model and view; other than that it just gets out of the way and is pretty invisible. I loved Silex, but sadly it's EOL so I need to move on. And, yeah, I'm being curmudgeonly and pre-judgemental as I go, I know. I do know Symfony is immensely popular, and I also know from my exposure to using its various independent libraries that it's been developed in a thoughtful, thorough manner. I expect I'm largely just being change-averse here. However I'm forewarning you now, as this will no-doubt come across in my tone, and my patience when things don't go exactly how I'd like them too (like some sort of spoilt brat). But if you've read this blog before… you probably already expect this.

Now that I have that out of my system (maybe), what's my aim for today? When I started yesterday, the intent of my work was described in this functional test:

/** @coversNothing */
public function testGreetingsEndPointReturnsPersonalisedGreeting()
{
    $testName = 'Zachary';
     $expectedGreeting = (object) [
        'name' => $testName,
        'greeting' => "G'day $testName"
    ];

    $client = new Client([
        'base_uri' => 'http://webserver.backend/'
    ]);

    $response = $client->get(
        "greetings/$testName/",
        ['http_errors' => false]
    );
    $this->assertEquals(Response::HTTP_OK, $response->getStatusCode());

    $contentTypes = $response->getHeader('Content-Type');
    $this->assertCount(1, $contentTypes);
    $this->assertSame('application/json', $contentTypes[0]);

    $responseBody = $response->getBody();
    $responseObject = json_decode($responseBody);
    $this->assertEquals($expectedGreeting, $responseObject);
}

In humanspeke, what I'm gonna do is:

  • create a route /greetings/[some name here]/;
  • return an object as JSON;
  • that confirms the name sent, plus has a greeting string for that name.

Very "Hello World". I did indicate I was not gonna be pushing the boat out too much here.

To start let's run that test, and watch it fail with a 404…

root@5565d6f15aca:/usr/share/fullstackExercise# vendor/bin/phpunit
PHPUnit 9.5.0 by Sebastian Bergmann and contributors.

.F....                                                              6 / 6 (100%)

Time: 00:04.414, Memory: 14.00 MB

There was 1 failure:

1) adamCameron\fullStackExercise\tests\functional\SymfonyTest::testGreetingsEndPointReturnsPersonalisedGreeting
Failed asserting that 404 matches expected 200.

/usr/share/fullstackExercise/tests/functional/SymfonyTest.php:63

FAILURES!
Tests: 6, Assertions: 12, Failures: 1.

Generating code coverage report in HTML format ... done [00:01.162]
root@5565d6f15aca:/usr/share/fullstackExercise#

That one failure is the one we want to see, so that's good: I have a test failing in exactly they way I expect it to be, so now I need to work out how to add a route and all that sort of jazz. Time to RTFM. Back soon.

Right so the docs for Symfony have been incredibly helpful so far. That's not sarcasm: they've been really bloody good! I'd been looking at the Installing & Setting up the Symfony Framework page, and one of its last links is to Create your First Page in Symfony. This steps one through setting up a route and the controller that services it. The first example did not have a "runtime" parameter in the URL slug like I need here, but that was covered in another linked-to page Routing (specifically the Route Parameters section of same). That was all the information I seemed to need for my code, so off I went.

Routing is done in config/routes.yaml, and there's an example in there already. So it was easy to stick my new route in:

#index:
#    path: /
#    controller: App\Controller\DefaultController::index

greetings:
  path: /greetings/{name}
  controller: adamCameron\fullStackExercise\Controller\GreetingsController::doGet

Curly braces around the {name} just mean that that part of the URL slug is dynamic, and its value is passed to the controller method. I could (and should!) put validation on this, but that's getting ahead of myself. The current requirement I have set only handles the happy path, so we'll stick to that.

The controller goes in the src/Controller directory. I'm more accustomed to using headlessCamelCase for my namespace parts, but I'll stick with Symfony's precedent here:

namespace adamCameron\fullStackExercise\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;

class GreetingsController extends AbstractController
{
    public function doGet(string $name) : Response
    {
        $greetingsResponse = [
            'name' => $name,
            'greeting' => "G'day $name"
        ];

        return new JsonResponse($greetingsResponse);
    }
}

This shows how the dynamic part of the slug from the route passes through into the controller method. Cool. From there I've gone my own way from the docs there cos Silex uses Symfony's request/response mechanism, so I know I can just return a JsonResponse like that, and it'll handle the 200-OK and the application/json part of the requirement. The docs integrate using Twig here to render some output, but there's no need for that complexity here. I suppose here the creation of $greetingsResponse is my "model", and the decision to just return a JsonResponse is my "view".

Being quite pleased with myself at how simple that was, I ran my test to verify how clever I was:

root@3fc72bf44d38:/usr/share/fullstackExercise# vendor/bin/phpunit tests/functional/SymfonyTest.php --filter=testGreetingsEndPointReturnsPersonalisedGreeting
PHPUnit 9.5.0 by Sebastian Bergmann and contributors.

F                                                                   1 / 1 (100%)

Time: 00:02.607, Memory: 8.00 MB

There was 1 failure:

1) adamCameron\fullStackExercise\tests\functional\SymfonyTest::testGreetingsEndPointReturnsPersonalisedGreeting
Failed asserting that 500 matches expected 200.

/usr/share/fullstackExercise/tests/functional/SymfonyTest.php:63

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

Generating code coverage report in HTML format ... done [00:01.790]
root@3fc72bf44d38:/usr/share/fullstackExercise#

rrarr! OK, not so clever after all it seems. Hitting the URL in the browser gives me more information:


What is this thing on about? See where it's looking for my controller? It's looking for a class App\Controller\GreetingsController, but that class is not in App/Controller, it's in adamCameron/fullStackExercise/Controller, and that's where the route code says it is. I have re-checked everything, and it's all legit.

Sigh. I could guess what it is. Symfony being a) "clever", and b) "opinionated". If you've just read the previous article, you might recall me raising my eyebrow at this bit in composer.json:

"autoload": {
    "psr-4": {
        "App\\": "src/",
        "adamCameron\\fullStackExercise\\": "src/"
    }
},

At the time I was just observing what a dumb namespace that was, but I'm now guessing that in Symfony's opinion that is the namespace we all should be using. I remembered something about some other autowiring config that Symfony has, and guessed there was something in there that might cater to classes in the App namespace, but not other namespaces. Even if the namespaces are explicit in the code (as they are here). I located the culprit in config/services.yaml:

services:
    # default configuration for services in *this* file
    _defaults:
        autowire: true      # Automatically injects dependencies in your services.
        autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.

    # makes classes in src/ available to be used as services
    # this creates a service per class whose id is the fully-qualified class name
    App\:
        resource: '../src/'
        exclude:
            - '../src/DependencyInjection/'
            - '../src/Entity/'
            - '../src/Kernel.php'
            - '../src/Tests/'

    # controllers are imported separately to make sure services can be injected
    # as action arguments even if you don't extend any base controller class
    App\Controller\:
        resource: '../src/Controller/'
        tags: ['controller.service_arguments']

    # add more service definitions when explicit configuration is needed
    # please note that last definitions always *replace* previous ones

So there's magic taking place for App; I guess I need to make it magical for my actual namespace instead. I'm never gonna write code with App as its namespace, so I'm just gonna punt that I can change that to adamCameron\fullStackExercise\: and adamCameron\fullStackExercise\Controller\: and it'll all be fine. I think ATM I only need to monkey with the Controller one, but I might as well do both now I guess. So with that change in place, I re-run the test:

root@3fc72bf44d38:/usr/share/fullstackExercise# vendor/bin/phpunit tests/functional/SymfonyTest.php --filter=testGreetingsEndPointReturnsPersonalisedGreeting
PHPUnit 9.5.0 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 00:06.440, Memory: 8.00 MB

OK (1 test, 4 assertions)

Generating code coverage report in HTML format ... done [00:01.825]
root@3fc72bf44d38:/usr/share/fullstackExercise#

All right, that was easily located / solved and is not the end of the world. However this from Symfony's error message irks me: "The file was found but the class was not in it, the class name or namespace probably has a typo". No mate. That's not the problem at all. The problem is that despite me giving you the exact fully-qualified namespaced class name in the route config, and the namespace in the class file was correct, and in the right place in the file system… and if you just left things be then you would have found the correct class. But: no. You had to try to be clever, and you failed miserably. I really frickin' hate it when technology goes "nono [patronising grimace] I've got this, I'll do it for you" (I know some devs like this too…), and it gets it wrong. I'm all for Symfony having the helpers there should one want them, but it shouldn't make these assumptions. I guess had it gone "hey aaah… that namespace yer using for yer controller? Ya need to configure that to work mate. Go have a look in config.yaml", then I'd just be going "yeah nice one: thanks for that". But then again most people would probably not have written this ranty paragraph at all, and just moved on with their lives, eh? Hey at least I'm self-aware.

All in all, even with the config/namespace hiccup, getting a simple route working was pretty bloody easy here. And this is a good thing about Symfony.

That was actually pretty short. It was about 500 words and some test-runs longer, as I had encountered a weird issue which I was moaning about. However when doing a final read-through of this before pressing "send", I looked at it some more, and it turns out it was just me being a div, and the problem was firmly of the PEBCAK variety. So… erm… I decided to delete that bit. Ahem.

Next I have to start doing some front-end-ish stuff so I've got an excuse to try out Vue.js. This should be interesting. I've never even looked @ Vue.js before. I bet I'll disagree with some of its opinions eh? Stay tuned…

Righto.

--
Adam

Wednesday 13 January 2021

Part 6: Installing Symfony

G'day:

These initial boilerplate paragraphs must be getting annoying by now. Sorry. But anyhow, here we go again. Please note that I initially intended this to be a single article, but by the time I had finished the first two sections, it was way too long for a single read, so I've split it into the following sections, each as their own article:

  1. Intro / Nginx
  2. PHP
  3. PHPUnit
  4. Tweaks I made to my Bash environment in my Docker containers
  5. MariaDB
  6. Installing Symfony (this article)
  7. Using Symfony
  8. Testing a simple web page built with Vue.js using Mocha, Chai and Puppeteer
  9. I mess up how I configure my Docker containers
  10. An article about moving files and changing configuration
  11. Setting up a Vue.js project and integrating some existing code into it
  12. Unit testing Vue.js components

As indicated: this is the sixth article in the series, and follows on from Part 5: MariaDB. It's probably best to go have a breeze through the earlier articles first. Also as indicated in earlier articles: I'm a noob with all this stuff so this is basically a log of me working out how to get things working, rather than any sort of informed tutorial on the subject.

For the earlier articles I had mostly tried-out what I was intending to do before I started writing about it. This time I am writing this at the same time as doing the work. To contextualise, all I have done at the moment is open up that article of Martin Pham's I've been following along ("Symfony 5 development with Docker"), and also got the Symfony docs up: "Installing & Setting up the Symfony Framework / Creating Symfony Applications".

First things first I want a failing test for my minimum requirements here, which is that when I GET /greeting/[some name here]/, I receive back a JSON response, along these lines: {"name":"[some name here]","greeting":"G'day [some name here]"}. Here's a test that tests this (tests/functional/SymfonyTest.php):

namespace adamCameron\fullStackExercise\test\functional;

use GuzzleHttp\Client;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Response;

class SymfonyTest extends TestCase
{
    /** @coversNothing */
    public function testGreetingsEndPointReturnsPersonalisedGreeting()
    {
        $testName = 'Zachary';
        $expectedGreeting = (object) [
            'name' => $testName,
            'greeting' => "G'day $testName"
        ];

        $client = new Client([
            'base_uri' => 'http://webserver.backend/'
        ]);

        $response = $client->get("greeting/$testName/");
        $this->assertEquals(Response::HTTP_OK, $response->getStatusCode());

        $contentTypes = $response->getHeader('Content-Type');
        $this->assertCount(1, $contentTypes);
        $this->assertSame('application/json', $contentTypes[0]);

        $responseBody = $response->getBody();
        $responseObject = json_decode($responseBody);
        $this->assertEquals($expectedGreeting, $responseObject);
    }
}

The code is pretty clear, I just perform the GET using "Zachary" as the name, and in return I verify I get a 200 OK response, it's JSON and it has the expected data structure in the body. Simple.

I run the test to check I get a fail with a 404:

root@20ad01609407:/usr/share/fullstackExercise# vendor/bin/phpunit test/functional/SymfonyTest.php
PHPUnit 9.5.0 by Sebastian Bergmann and contributors.

E                                                                   1 / 1 (100%)

Time: 00:00.670, Memory: 6.00 MB

There was 1 error:

1) adamCameron\fullStackExercise\test\functional\SymfonyTest::testGreetingsEndPointReturnsPersonalisedGreeting
GuzzleHttp\Exception\ClientException: Client error: 'GET http://webserver.backend/greetings/Zachary/' resulted in a '404 Not Found' response:
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx/1. (truncated...)

OK what? Why the hell is Guzzle deciding a 404 response - which is completely legitimate - is in some way "exceptional", and warrants an exception being thrown. That's daft. Lemme go RTFM and see if there's a way of me configuring Guzzle to not get ahead of itself and just behave. back in a bit…

… OK, easily solved. There's a config param for the get call that switches that behaviour off. Thanks to the Guzzle docs for making that easy to find ("Request options / http-errors").

$response = $client->get(
    "greetings/$testName/",
    ['http_errors' => false]
);

And now we get a test that fails as we'd expect it to:

root@20ad01609407:/usr/share/fullstackExercise# vendor/bin/phpunit test/functional/SymfonyTest.php
PHPUnit 9.5.0 by Sebastian Bergmann and contributors.

F                                                                   1 / 1 (100%)

Time: 00:00.549, Memory: 6.00 MB

There was 1 failure:

1) adamCameron\fullStackExercise\test\functional\SymfonyTest::testGreetingsEndPointReturnsPersonalisedGreeting
Failed asserting that 404 matches expected 200.

/usr/share/fullstackExercise/test/functional/SymfonyTest.php:28

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

Generating code coverage report in HTML format ... done [00:01.166]
root@20ad01609407:/usr/share/fullstackExercise#

We're now OK to work out how to get Symfony installed and running, and make an end point that behaves as we need it to.

Or are we? I think I might have got a bit ahead of myself here. I'm back looking at Martin's article, and he says

Let's proceed to the Symfony installation:

$ symfony new src

[…] open http://localhost, you will see a Symfony 5 welcome screen.

OK so I think our first test should be that that Symfony 5 welcome screen works. I've got no idea what the screen says, so let me google for expectations. Right according to Creating your First Symfony App and Adding Authentication by Olususi Oluyemi (from a random Google Images search for "symfony 5 welcome screen") it should look like this:

I'm gonna write a quick test to check for that, and bypass that other test for now:

/** @coversNothing */
public function testSymfonyWelcomeScreenDisplays()
{

    $client = new Client([
        'base_uri' => 'http://webserver.backend/'
    ]);

    $response = $client->get(
        "/",
        ['http_errors' => false]
    );
    $this->assertEquals(Response::HTTP_OK, $response->getStatusCode());

    $html = $response->getBody();
    $document = new \DOMDocument();
    $document->loadHTML($html);

    $xpathDocument = new \DOMXPath($document);

    $hasTitle = $xpathDocument->query('/html/head/title[text() = "Welcome to Symfony!"]');
    $this->assertCount(1, $hasTitle);
}

I was scratching my head guessing what the mark-up around the "Welcome to Symfony x.y.z" text in the body is. It could be <h2> and <h3> tags, or it could just be different font-size stylings. Then I noticed the <title> content, and just decided to check for that. If that string is there: we're good. To start with, we want to be not good, so let's run that test:

root@20ad01609407:/usr/share/fullstackExercise# vendor/bin/phpunit test/functional/SymfonyTest.php
PHPUnit 9.5.0 by Sebastian Bergmann and contributors.

F                                                                   1 / 1 (100%)

Time: 00:00.555, Memory: 6.00 MB

There was 1 failure:

1) adamCameron\fullStackExercise\test\functional\SymfonyTest::testSymfonyWelcomeScreenDisplays
Failed asserting that actual size 0 matches expected size 1.

/usr/share/fullstackExercise/test/functional/SymfonyTest.php:32

FAILURES!
Tests: 1, Assertions: 2, Failures: 1.

Generating code coverage report in HTML format ... done [00:01.195]
root@20ad01609407:/usr/share/fullstackExercise#

Right so both Martin's article and the Symfony docs say I need to install Symfony (via Composer), and create an application with it, so I'll do this now.

The first hiccup is that the Symfony installer assumes that yer Symfony app is the centre of the world, and it needs to be the one that creates yer project directory for you. Great. Thanks for that. There's probably (?) a way of coercing it to understand that it's just a frickin' framework and I just need to add it to my existing app which is already underway. This is not so far--fetched I think, but after 15min of googling I drew a blank, so I decided to take a different approach. I'm gonna blow away my stuff to allow Symfony to play its silly little game, and then reintegrate my stuff back into the stuff Symfony creates. After all it's just some tests and composer.json config at present really.

/me thinks things through… OK that won't work. I can't shift all the files out of the fullstackExercise directory because all the Docker stuff is in there, and I can't bring my containers up if they're not there. Without the containers up, I don't have PHP or Composer so I can't do the Symfony install. To be blunt here: screw you, Symfony (well it's actually Composer that is bleating about the directory not being empty I guess. So screw Composer). With Silex? Just add an entry to composer.json, add a line to index.php and that's it. This is how a microframework should work.

Plan B is to do things the other way around. I need to shift my fullStackExercise project directory to have a different name (fullStackExercise.bak) temporarily so I can allow Symfony to create its own one. To do this I need to rejig my docker/docker-compose.yml file and the docker/php-fpm/Dockerfile to reference fullstackExercise.bak. Note that docker/nginx/sites/default.conf file also references root /usr/share/fullstackExercise/public, so at this point I decided that for now I'll just bring up the PHP container for this, so stripped the Nginx and MariaDB stuff out of docker-compose.yml completely. So these are the file changes:

docker-compose.yml:

version: '3'

services:

  php-fpm:
    build:
      context: ./php-fpm
    environment:
      - DATABASE_ROOT_PASSWORD=${DATABASE_ROOT_PASSWORD}
    volumes:
      - ../..:/usr/share/src # was ..:/usr/share/fullstackExercise
      - ./php-fpm/root_home:/root
    stdin_open: true # docker run -i
    tty: true        # docker run -t
    networks:
      - backend

networks:
  backend:
    driver: "bridge"

Note that I've shifted the main volume up one level in the host's directory hierarchy. This is because I need to let Symfony recreate that fullStackExercise at that parent level.

php-fpm/Dockerfile:

FROM php:8.0-fpm
RUN apt-get update
RUN apt-get install git --yes
RUN apt-get install net-tools iputils-ping --yes
RUN apt-get install zip unzip --yes
RUN docker-php-ext-install pdo_mysql
RUN pecl install xdebug-3.0.1 && docker-php-ext-enable xdebug
COPY --from=composer /usr/bin/composer /usr/bin/composer
ENV XDEBUG_MODE=coverage
#WORKDIR  /usr/share/fullstackExercise.bak/
WORKDIR  /usr/share/src/
CMD composer install ; php-fpm
EXPOSE 9000

I've made the same change to the WORKDIR here too, to match docker-compose.yml. I've also added in a line to install zip and unzip into the container, because when I was testing this, Composer was bleating about them not being present.

Now in theory if I drop my containers and bring them back up again, I should be able to bash into the PHP container and do the Symfony install:

adam@DESKTOP-QV1A45U:~$ cd /mnt/c/src/fullstackExercise.bak/docker
adam@DESKTOP-QV1A45U:/mnt/c/src/fullstackExercise.bak/docker$ docker-compose down --remove-orphans
Removing network docker_backend
WARNING: Network docker_backend not found.
adam@DESKTOP-QV1A45U:/mnt/c/src/fullstackExercise.bak/docker$ # OK I forgot I had already brought them down

adam@DESKTOP-QV1A45U:/mnt/c/src/fullstackExercise.bak/docker$ docker-compose up --build --detach
Creating network "docker_backend" with driver "bridge"
Building php-fpm
[...]
Successfully built 4b8851fefd22
Successfully tagged docker_php-fpm:latest
Creating docker_php-fpm_1 ... done

adam@DESKTOP-QV1A45U:/mnt/c/src/fullstackExercise.bak/docker$ docker exec --interactive --tty docker_php-fpm_1 /bin/bash
root@87420db3da90:/usr/share/src# ll
total 4
drwxrwxrwx 1 1000 1000  512 Jan  1 16:16 ./
drwxr-xr-x 1 root root 4096 Jan  1 16:40 ../
drwxrwxrwx 1 1000 1000  512 Dec 20 12:58 fullstackExercise.bak/
root@87420db3da90:/usr/share/src#
root@87420db3da90:/usr/share/src# # so far so good
root@87420db3da90:/usr/share/src#
root@87420db3da90:/usr/share/src# composer create-project symfony/skeleton fullstackExercise
Creating a "symfony/skeleton" project at "./fullstackExercise"
Installing symfony/skeleton (v5.2.99)
[...]

Some files may have been created or updated to configure your new packages.
Please review, edit and commit them: these files are yours.


What's next?


  * Run your application:
    1. Go to the project directory
    2. Create your code repository with the git init command
    3. Download the Symfony CLI at https://symfony.com/download to install a development web server

  * Read the documentation at https://symfony.com/doc

root@87420db3da90:/usr/share/src#

So far so good. now I just need to move a bunch of stuff back from my app into this one. I'll go ahead and do it, and report back when done, and list what I needed to do…

  • .git/, .idea/ (this is PHPStorm's project config), docker/, log/ directories could just be copied straight across.
  • As could LICENSE, phpcs.xml, phpmd.xml, phpunit.xml, README.md.
  • And src/MyClass.php, public/gdayWorld.html and public/gdayWorld.php.
  • The only overlap in .gitignore was the vendor/ directory, otherwise I could just move my bits back in.
  • composer.json was fairly straight forward, but I raised an eyebrow that Symfony seems to suggest using App as a top-level namespace:
    "autoload": {
        "psr-4": {
            "App\\": "src/",
    

    To me, using a namespace "App" is like calling a variable something like indexVariable, or totalVariable or something, or naming a class AnimalClass. Ugh. It gets even worse with the testing namespace: App\\Tests\\. For now I'm just adding my own namespacing in as well. Hopefully I don't have to use Symfony's approach here.
  • Symfony uses tests as its test directory; I've always used test, but I'm ambivalent about this so am sticking with Symfony's lead here: I'll change my directory to tests. This will require me changing the namespaces in the test source code files too.
  • Because I've changed composer.json by hand, I'll need get rid of composer.lock and vendor/ and do a composer install again.
  • Lastly I've reverted the Docker files I'd changed above back to how they were before: removing the references to fullstackExercise.bak, and bringing back the Ngxin and MariaDB containers.

OK having done all that, and now will rebuild my containers. Before starting this paragraph I had done all this and had started to fight with an issue I thought I was having with PHPUnit, so I didn't capture the screen showing it all working. It's all just Docker bumpf, then some Composer bumpf, none of which was very surprising, so I'll spare you the detail. Back to the unit test run:

root@51b5afdff6ea:/usr/share/fullstackExercise# vendor/bin/phpunit
PHPUnit 9.5.0 by Sebastian Bergmann and contributors.

F....                                                               5 / 5 (100%)

Time: 00:02.896, Memory: 14.00 MB

There was 1 failure:

1) adamCameron\fullStackExercise\tests\functional\SymfonyTest::testSymfonyWelcomeScreenDisplays
Failed asserting that 404 matches expected 200.

/usr/share/fullstackExercise/tests/functional/SymfonyTest.php:23

FAILURES!
Tests: 5, Assertions: 10, Failures: 1.

Generating code coverage report in HTML format ... done [00:01.152]
root@51b5afdff6ea:/usr/share/fullstackExercise#

Doh!

Things get weirder cos I hit the site on my host browser:

And that seems fine. First troubleshooting step: factor-out all the code I'm running, and manually curl it from PHP:

root@51b5afdff6ea:/usr/share/fullstackExercise# php -a
Interactive shell

php > $ch = curl_init();
php > curl_setopt($ch, CURLOPT_URL, "webserver.backend");
php > curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
php > $output = curl_exec($ch);
php > curl_close($ch);
php > echo $output;
<!DOCTYPE html>
<html dir="ltr" lang="en">
<head>
    <meta charset="UTF-8" />
    <meta name="robots" content="noindex,nofollow,noarchive,nosnippet,noodp,notranslate,noimageindex" />
    <title>Welcome to Symfony!</title>

So there's all the content. Fine.

Next I want to check just Guzzle by itself, to demonstrate to myself it is working fine. I hand-cranked some Guzzle calls from outside of my test code:

root@51b5afdff6ea:/usr/share/fullstackExercise# php -a
Interactive shell

php > require 'vendor/autoload.php';
php > $client = new \GuzzleHttp\Client(['base_uri' => 'http://webserver.backend']);
php > $response = $client->get("/");

Warning: Uncaught GuzzleHttp\Exception\ClientException: Client error: `GET http://webserver.backend/` resulted in a `404 Not Found` response:
<!DOCTYPE html>
<html dir="ltr" lang="en">
<head>
    <meta charset="UTF-8" />
    <meta name="robots" content="noindex, (truncated...)

It's still frickin' 404ing.

I scratched my head a lot and googled why guzzle might 404 on something that everything else 200s on. Then I saw it.

That mark-up that is spewing out in the Guzzle error is not generic 404 response. What it is is the content from the Welcome to Symfony! page. I checked something else in my browser…

That "welcome" page? It has a 404 status. Now I can see what they're doing here, and they even warn about it if one pays attn:

However to me a welcome page - that actually has welcome content on it, therefore is precisely what one would be expecting on that URL- is not a 404 situation. Ah well.

So my test is reporting correctly! (Yay for tests!) It's just that the expectations I gave the test were wrong. I need to expect a 404, not a 200. I'll make that change, and now the tests all pass:

root@51b5afdff6ea:/usr/share/fullstackExercise# vendor/bin/phpunit
PHPUnit 9.5.0 by Sebastian Bergmann and contributors.

.....                                                               5 / 5 (100%)

Time: 00:07.614, Memory: 14.00 MB

OK (5 tests, 11 assertions)

Generating code coverage report in HTML format ... done [00:01.173]
root@51b5afdff6ea:/usr/share/fullstackExercise#

Phew. BTW I had to change my test a bit more than I expected:

/** @coversNothing */
public function testSymfonyWelcomeScreenDisplays()
{

    $client = new Client([
        'base_uri' => 'http://webserver.backend'
    ]);

    $response = $client->get(
        "/",
        ['http_errors' => false]
    );
    // unexpectedly perhaps: the welcome page returns a 404. This is "by design"
    $this->assertEquals(Response::HTTP_NOT_FOUND, $response->getStatusCode());

    $html = $response->getBody();
    $document = $this->loadHtmlWithoutHtml5ErrorReporting($html);

    $xpathDocument = new \DOMXPath($document);

    $hasTitle = $xpathDocument->query('/html/head/title[text() = "Welcome to Symfony!"]');
    $this->assertCount(1, $hasTitle);
}

private function loadHtmlWithoutHtml5ErrorReporting($html) : \DOMDocument
{
    $document = new \DOMDocument();
    libxml_use_internal_errors(TRUE);
    $document->loadHTML($html);
    libxml_clear_errors();

    return $document;
}

There's a bug in PHP's HTML loading that makes it barf on HTML5 tags (see "PHP DOMDocument errors/warnings on html5-tags", so I needed to put some mitigation in for that. It's not a great solution, but it works.

Now to try to work out how to implement my /greetings/Zachary route…

But not for now. I've just checked the length of this, and not counting all the code and telemetry, this is already close to 1800 words (it'll be over by the time I finish this outro). I think this exercise of getting the Symfony framework code into my project and up and running is sufficient for a stand-alone article. I'll do a separate article for the "and now let's get it to do something except return a confusing (to me. Cringe) 404 response". I'll crack on with that tomorrow, over here: "Part 7: Using Symfony".

Righto.

-- 
Adam

Wednesday 27 July 2016

PHP / Silex / Dependency injection: should I use a reference to a method?

G'day:
This is another example of where I want to put a question out there, but it needs more space that Twitter will allow. I guess I'll put it on Stack Overflow too.

We use Silex and Pimple's DI container. Generally in our service providers we expose references to objects, eg:

$app['service.something'] = $app->share(function ($app) {
    return new SomethingService();
});

Then usage of that is predictable:

$result = $app['service.something']::someMethod(1, 2, 3);

But here's the thing. As with the case above, the method is actually a static one. So it seems "odd" to be calling it on an instance of the SomethingService, rather than on the class. As coincidence would have it, this is the only public method in SomethingService too.

So we've done this sort of thing in our service provider:

$app['something'] = $app->protect(function ($arg1, $arg2, $etc) {
    return SomeClass::someMethod($arg1, $arg2, $etc);
});

Usage:

$result = $app['something'](1, 2, 3);

Now this works OK, but I have a coupla hesitations:
  • the name $app['something'] is a bit noun-y. As it's a method it ought to be verb-y IMO, eg: $app['doTheThing']. I guess that's small fish.
  • I'm just not sure if it's particularly "semantic" to be exposing just methods like that. All usage of Pimple I've seen has been to expose dependent objects.
I think we'll run with what we've got, but I kinda want to get more thoughts on if this is the right approach. And if so (or if not) why not. There might be some gotchas here I'm not thinking of, or something.

Cheers.

--
Adam

Wednesday 10 June 2015

PHP: more dumping in PHP... via Twig this time

G'day:
A week or so ago I was messing around with the dBug class that Kwaku Otchere (according to the author info in the class) came up with a coupla years ago: "PHP: I find the PHP equivalent of <cfdump> on GitHub shortly after I package it myself". That was an interetsing exercise (a mostly pointless one in the end, but hey). But for it to be useful for us at work, we need to be able to dump stuff from a Twig file as well as from a PHP file. Now I know one can extend Twig and add... things to it... but other than knowing where the docs are, I didn't know anything further.

Monday 11 May 2015

PHP: inadvertently looking at a coupla refactoring tactics with Twig

G'day:
Here we have details of me unsuccessfully trying to replicate an issue we are having with one of our Twig files... it's not seeing some data being passed to it. However along the way I got myself a bit more up to speed with a coupla refactoring tactics one can use with Twig.

This article doesn't cover everything about Twig (I don't know everything about Twig, for one thing), it just covers the troubleshooting steps I took too try to replicate the issue I was having.

Spoiler: I did not solve the problem.
Spoiler spoiler: or did I?

Saturday 25 April 2015

PHP: Silex Controller Providers

G'day:
I'm getting to this earlier than expected (see my observation in yesterday's article: "PHP: Service Providers with Silex")... I was able to write the code last night whilst watching telly, and now I'm stuck on a train due to "signal failure", so have a few (hopefully just a few ~) minutes to start writing this on my phone. Apologies for any autocorruptions I miss during proofreading.

OK, so what's a Controller Provider. Well, for about the first thousand times I read the docs, I could still not answer that. Obviously one can kinda guess from the name: it provides controllers. Ooookay... but what's the ramifications of that, and why would I choose this approach to "providing controllers" over any other way? Obviously it seems to be Silex's prefers way, but they need to do a better sell job on me than that. But the docs weren't doing it. Furthermore, there's bugger-all useful material out there about them (that I could find, anyhow). All the articles I found just regurgitated the docs, or paraphrased them. And the docs just explain "how", but never really explains "why". This is a failing of a lot of docs... explaining the "how" or "what" is easy, but it's the "why" that's important for a given feature. Explain the "why" first, then the "what". Why do I care about this thing you're banging on about? If I'm left with "err... dunno?", that's a fail.

Wednesday 22 April 2015

PHP: Service Providers with Silex

G'day:
I'm still being obstructed by train delays... I'm now sitting at the station awaiting a train to Leytonstone, at which I can apparently change trains to continue my journey - albeit at snail's pace - into work. I'm hoping I get there in time for the Sandwich Man doing his rounds at 11am.

Anyway, recently I've got myself up to speed with the notion of service providers, in Silex. Silex is the framework we use for the reimplementation of the HostelBookers.com website. Previously I wrote an article ("PHP: messing around with Silex, Pimple & Guzzle") about how we were using Pimple in Silex to handle our dependency injection. At that time we were hand-rolling our usage of it, thus:

Monday 5 January 2015

PHP: messing around with Silex, Pimple & GuzzleHttp

G'day:
As you know, I've recently shifted most of my attention in my day job from CFML to PHP. This has had a coupla false starts: firstly I was needed back on the CFML team for a while to oversee some maintenance work, and then I was off work with eye trouble for a week or so. And then bloody Xmas & New Year came around, interfering with everything as well. All in all: I am well behind in my PHP learning / baptism-by-fire. I've good a month to catch up before I'm off work again for a month whilst I follow New Zealand's trials and tribulations in the Cricket World Cup. Conveniently being held in New Zealand (oh, and Aussie too, I s'pose) this year. This is only "convenient" as I was scheduled to be back in NZ around that time anyhow, checking in on the family and drinking beer with mates.

[disclosure... I am victim of a flight delay so have been killing time coding and drinking Guinness. I am halfway through my fourth pint, so my verbal diarrhoea will be worse than usual, as that previous paragraph evidenced. On review of the rest during proofreading, it doesn't get any better].

Anyway, I sat down and looked at our new app's PHP codebase the other day and went "Blimey! This is complicated!". Now I'm a newbie with PHP so I don't expect to follow the minutiae of each line of code, but I figured I should understand the general flow of what's going on. But no. For what should be a reasonably simple website (a front for online accommodation booking), we seemed to have an awful lot of Routings and Factories and Providers and Builders and Controllers and Repositories and combinations of the above (eg: ProviderFactoryBuilders... or it might be RepositoryBuilderFactories or something). It looked to me like a Java application in its structure, not one written in a dynamic web language. Still: I could be missing something, so decided to look into the app's architecture.

Basically we're using a framework called Silex. This is based on Symfony, but is very cut down. I guess if it was the CFML world, Silex might be taking FW/1's approach, and Symfony might be more like how ColdBox views the world. My position on frameworks is that FW/1 is about right; and ColdBox is too all-inclusive. But mileage varies, and let's not get into that. I'd heard of Symfony - but never used it - but had not heard of Silex. However reading the docs it sounds quite good (in that the docs are fairly short, I understand them).

I've been using ColdSpring for years, and like the idea of DI. Coming from CFML, it makes slightly less sense to me in the context of PHP as everything is request-centric: there's no sense of application (and hardly any sense of session in what I'd consider to be a professional implementation). So unlike how ColdSpring will cache bean configs for the life of the application, one has to tread more cautiously with PHP because the whole lot starts from scratch every request. That said, it's still desirable to define the wiring of one's classes & dependencies once, and then when one comes to need a new service (or ProviderFactoryRepositoryBuilder ;-), it's just a matter of saying "one of those please", rather than having to hand code initialising the thing with all its dependencies (and their dependencies, etc). Fortunately Silex ships with a DI framework - Pimple - baked in. Nice. But I need to know how to use it. I also needed to know whether we really need a lot of our factory classes, as they didn't really seem to be doing much... and I knew that if I was using CFML and ColdSpring I'd not need them at all. I was hoping this'd be the same for our new app.

Another new concept to me - I've been stuck using Fusebox (the XML version, not the CFC version) for years - is having the framework handle the request routing too. We've been using .htaccess for this previously. TBH: I actually think this is the correct approach, too: routing is the job of the HTTP tier, not the application tier. However frameworks seem to want to do this stuff themselves these days. They seem to have migrated from "MVC" to be more like "RMVC" (or, more sensibly: "RCMV"), with the R standing for routing. Our routing code seemed to be very very repetitive (copy and paste boilerplate ceremony with a few differences per route), and also seemed to be a mishmash of config and code, which struck me as being less than ideal. As well as just making for a lot of code that we oughtn't need. I was buggered if I knew how to fix it, but I figured I'd try to understand how we got to where we did, and whether there might be a better approach.

Finally, this new app hits a REST API to get its data. I'm hesitant about using REST for internal server-to-server comms... it's just slow and not the right tool for the job if one can just run the code to do the work, instead of making an HTTP request... to run the code to do the job. But we have no choice here, so REST is what we use. I know the theory of REST adequately (and a lot better having read Adam Tuttle's book: "Book review: REST Web APIs: The Book (win a copy here)"), and can wire together REST stuff in CFML, but had no idea how it works in PHP. It's not just a matter of calling <cfhttp>, which is about where my experience of consuming REST web services extends to. On our project we use something called GuzzleHttp. Which also sounds pretty bloody cool, actually.

Oh... that wasn't the last bit. We're also using Twig as our templating engine, and whilst I had not specific qualms about our usage of it, I simply didn't know how it worked, so wanted to have a brief look at that too. Well: one qualm. I dunno why mark-up-aware languages like PHP and CFML actually need a templating engine... given that's what the languages themselves are for. PHP's less good at it than CFML is (no custom tags, for example), but IMO all the templating engines I've seen make sense in the context of the languages they do the templating for, but most of the considerations justifying a specific additional tier for views just doesn't apply with either PHP or CFML. But "it's the way it's done", so be it.

I'm in Ireland this weekend, and I had Saturday and Sunday afternoons free to either stare at walls (of my B&B or Shannon Airport, respectively), or write some code. So my weekend mission was to install Silex, Pimple, GuzzleHttp, Twig, etc, and write a proof of concept site doing some routing, some DI, and some REST requests and displaying them with some Twiggage. I had a false start yesterday as the wireless at the pub (which is where I do my work on Saturdays) was off when I arrived, so I had to do something which didn't require any RTFM. So I continued to work on "Learn CFML in 24 hours". I knocked out another coupla thousand words, which was good. It was only today @ Shannon I've been able to work on the PHP stuff. "fortunately" my flight has been delayed so I have had a good few hours sitting here with a Guinness and messing around with this PHP stuff.

Pleasingly... it's only taken me a coupla hours to knock out the basic skeleton of what I wanted to test, I'm moderately happy with it, and it covers all the bases. And I think I know how everything works now, too. Hopefully. Here it is.

Installation

This is just a pleasure. It's all done by Composer, and it all just works. This probably wouldn't be mention-worthy for most people, but coming from the CFML world (where CommandBox is only just now beginning to offer any sort of package management), it's a great new experience. I've already got Composer installed, so all I need for my app is the composer.json file:

{
    "require": {
        "silex/silex": "~1.1",
        "symfony/config": "~2.6",
        "symfony/yaml": "~2.6",
         "guzzlehttp/guzzle": "~5.0",
        "twig/twig": ">=1.8,<2.0-dev"
    },
    "autoload": {
        "psr-4": {
            "dac\\silexdemo\\": "src/",
            "app\\": "app/"
        }
    }
}

I then do composer update from the command line, and everything installs. Oh the PSR-4 stuff is just my namespacing for my own code, which Composer looks after the autoloading of these days too. Nice.

My plan is to create a site which has a single route: /user/n/ where n is an ID, and the site will dash off and call the REST service, get the requested user, and then - via Twig - output the details (the details being : ID, firstName, lastName). That's it. I wanted to use the minimum of code (whilst still architecting things adequately), and especially a minimum of procedural code. I wanna be using OO, not just PHP script files.

File organisation

My app is organised as follows:

phpsilex.local/ - app root
 composer.json
 composer.lock
 vendor/ - Silex, Twig, Pimple, Symfony, GuzzleHttp stuff, as installed by Composer
 app/ - app bootstrap stuff
  app.php
  Dependencies.php
  Routes.php
 config/ - non PHP files
  routes.yml
 src/ - the code for the app itself
  beans/
   User.php
  controllers/
   User.php
  services/
   User.php
  views/
   user.html.twig
 public/ - site root (only this stuff is web browseable)
  index.php

Website code

As one has come to expect these days, the publicly accessible code is minimal:

<?php
// index.php
require __DIR__ . '/../app/app.php';

It just bootstraps the app.

Application config and bootstrap

<?php
// app.php

use Silex\Application;

require_once __DIR__.'/../vendor/autoload.php';


$app = new Application();
$app["debug"] = true;

$app->register(new Silex\Provider\ServiceControllerServiceProvider());
$app->register(new Silex\Provider\TwigServiceProvider(), [
    "twig.path" => __DIR__.'/../src/views'
]);

app\Dependencies::configure($app);
app\Routes::configure($app);

$app->run();

app.php does the boilerplate stuff of declaring the app to be a Silex one, and configuring it and running it. My two intrusions here are how I am doing the DI, and how I'm doing the routing.

The general Silex guidance seems to err towards procedural code, which sux a bit. I want to keep things as encapsulated as possible, and also making them as testable as possible. From start to finish. This means classes and methods. So I've put my dependency injection config into a class, as I have with the routing.

Dependencies (and their injection)


<?php
// Dependencies.php

namespace app;

use \dac\silexdemo\controllers;
use \dac\silexdemo\services;
use \dac\silexdemo\beans;
use GuzzleHttp\Client;

class Dependencies {

    static function configure($app){

        $app["controllers.hello"] = $app->share(function() {
            return new controllers\Hello();
        });        
        $app["controllers.user"] = $app->share(function($app) {
            return new controllers\User($app["twig"], $app["services.user"]);
        });        


        $app["services.user"] = $app->share(function($app) {
            return new services\User($app["factories.user"], $app["services.guzzle.client"]);
        });        


        $app["factories.user"] = $app->protect(function($id, $firstName, $lastName) {
            return new beans\User($id, $firstName, $lastName);
        });


        $app["services.guzzle.client"] = function() {
            return new Client();
        };
    }

}

Look how easy that lot is. Pimple is baked into Silex, so all I need to do is to stick a bunch of function expressions into the $app itself. Then when I come to use them, I just reference them in exactly the same way. This is still using configuration over convention (so like how ColdSpring would do it, more than how DI/1 or even more so WireBox would want to do it), and this suits me as well. But the configuration is clean because it uses actual code.This comment flies in the face of other comments I've made regarding separating config from code, but I think it's the appropriate handling here as the config actually is code: it's defining how code should be run.

Note that because everything here is a function expression, nothing actually runs to start with. I was concerned that if one had a huge dependency-injected application that there's be a lot of upfront overhead whilst the dependencies are executed. This would be disastrous with PHP needing to do this every request, but the DI config is only ever actually executed when it's needed. Nice.

There are three main conceits I'm leveraging here.

Firstly, the default syntax is to create a new object each time a service reference is used, eg:

$app["services.guzzle.client"] = function() {
    return new Client();
};

Each time I reference $app["services.guzzle.client"] in my code, I'll get a new Client object. This is fine in this situation, but for a lot of my services a singleton will be fine, so I use this syntax instead:

$app["controllers.user"] = $app->share(function($app) {
    return new controllers\User($app["twig"], $app["services.user"]);
});

The share() call there returns the same object every time. We have a lot of boilerplate code in our app currently which ensures this, and as far as I can tell it's simply not necessary.

We also have a lot of hand-written factory classes for handling creation of transient beans. This is baked into Pimple too:

$app["factories.user"] = $app->protect(function($id, $firstName, $lastName) {
    return new beans\User($id, $firstName, $lastName);
});

That basically defines a singleton factory for creating transient User beans. I didn't need any specific factory code at all: all I needed to do is to wrap my function expression with the protect() call. Nice one.

There are no-doubt situation wherein we might actually need a bespoke factory method or two, but I can't recall seeing any in our existing code. And certainly not for my purposes. Still: the way to handle this is to only write the code that's necessary. We don't need a factory for every transient if Pimple can handle it for us with configuration. We should only write the code that we need. Code that fulfills only a ceremonial or "just in case" role is bad code.

Aside: I can't help but think if we'd been using TDD whilst writing this lot, we'd have a lot less code to contend with, because TDD encourages the writing of only necessary code. Unfortunately a lot of this code was written before our current team existed, so we had no say in that particular decision.

That's my dependencies wired.

Routing


<?php
// Routes.php

namespace app;

use Silex\Application;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\Routing\Loader\YamlFileLoader;
use Symfony\Component\Routing\RouteCollection;

class Routes {

    static function configure($app){
        $app["routes"] = $app->extend("routes", function (RouteCollection $routes, Application $app) {
            $loader     = new YamlFileLoader(new FileLocator(__DIR__ . "/../config"));
            $collection = $loader->load("routes.yml");

            $routes->addCollection($collection);
            return $routes;
        });
    }

}

This code I copied from some blog or other (I'll try to dig it up, but don't have it in front of me just now... found it: "Scaling Silex applications (part II). Using RouteCollection"). The documented approach to routing in Silex is to write each of them with PHP code, within the app.php file. That's bloody awful. Not only do I want them out of the app.php file, I also want them out of PHP code completely. Route definitions are not code. Plain and simple. So this code above lets me define my routes using YAML (which I am unimpressed with as a format, but hey) instead:

# routes.yml
_hello:
    path:        /hello/{name}
    defaults:    { _controller: "controllers.hello:doGet"}

_user:
    path:        /user/{id}
    defaults:    { _controller: "controllers.user:getUser"}
    requirements:
        id:  \d+

I'm not doing much with the routing here, but you get the general idea. One thing I am doing here is that my controllers are DI-ed services (Silex's guidance is to do those inline too!), which the Symfony RouteCollection approach supports just fine. Note that controllers.user is a reference to the controller's definition in Dependencies.php, and getUser is the relevant method in same. I'll get to that.

Application code

Indeed I'll get to that now. Here's the controller;

<?php
// User.php

namespace dac\silexdemo\controllers;

class User {

    protected $twig;
    protected $userService;

    function __construct($twig, $userService){
        $this->twig = $twig;
        $this->userService = $userService;
    }

    function getUser($id){
        $user = $this->userService->getUser($id);

        return $this->twig->render('user.html.twig', array(
            'user' => $user,
        ));
    }

}

This shows the correlation between the DI config and the actual classes. From Dependencies.php:

$app["controllers.user"] = $app->share(function($app) {
    return new controllers\User($app["twig"], $app["services.user"]);
});

The controller needs to know about the User service so it can go grab some data, and it needs to know about Twig, which is configured for all the views. This is from app.php:

$app->register(new Silex\Provider\TwigServiceProvider(), [
    "twig.path" => __DIR__.'/../src/views'
]);

(I say "all": there's only the one view in this app).

getUser() calls the model (services.user) and renders the view (user.html.twig), passing the User fetched from the model to it.

Here's the model:

<?php
// User.php

namespace dac\silexdemo\services;

class User {

    protected $userFactory;
    protected $guzzleClient;

    function __construct($userFactory, $guzzleClient){
        $this->userFactory = $userFactory;
        $this->guzzleClient = $guzzleClient;
    }

    function getUser($id){
        $startTime = self::getElapsed("Start");

        $response = $this->guzzleClient->get('http://cf11.local:8511/rest/api/person/' . $id,["future"=>true]);
        self::getElapsed("After async guzzle call", $startTime);

        $response->wait();
        self::getElapsed("After wait() call", $startTime);

        $userAsArray = $response->json();

        $userFactory = $this->userFactory;
        $user = $userFactory($userAsArray["ID"], $userAsArray["FIRSTNAME"], $userAsArray["LASTNAME"]);

        return $user;
    }

    private static function getElapsed($message, $start=-1){
        if ($start == -1){
            $start = time();
        }
        error_log(sprintf("%s: %d", $message, time() - $start));
        return $start;
    }

}

There's a lot more code there than is necessary, as I am also recording some metrics when I run that code (getElapsed() and the calls to same).

I hit my REST web service using an async call to Guzzle, as this is also a requirement which we seem to have hand-coded instead of using what's provided for us already. I just wanted to make sure it all works. Here the Guzzle call returns a FutureResponse object rather than a result, per se. This lets the rest of the code continue whilst Guzzle makes the (non-performant) REST call. I'm being slightly contrived here, but it's not until the wait() call is made that the calling code will actually block until Guzzle is done. This would not be how I'd do this usually, but it demonstrates the concept. I've tweaked my web service to take five seconds to run, and if we look in the logs, we can see that the blocking only occurs when wait() is called:

Start: 0
After async guzzle call: 0
After wait() call: 5

There's a bunch of other stuff one can do with the future / promise pattern here, but that's a discussion for another day. This just proves Guzzle does a good job of making async HTTP calls, without any coding on our part.

The web service returns JSON, which Guzzle can expect, and I populate a new bean with this. This is where we use that config-free bean factory. Remember this from Dependencies.php?

$app["factories.user"] = $app->protect(function($id, $firstName, $lastName) {
    return new beans\User($id, $firstName, $lastName);
});

Getting and populating a new User bean is as simple as:

$userFactory = $this->userFactory;
$user = $userFactory($userAsArray["ID"], $userAsArray["FIRSTNAME"], $userAsArray["LASTNAME"]);

Calling $userFactory() is creating a new User object. Oh... the upper case key names are ColdFusion's fault, btw. It does that (uppercases stuff). Stoopid.

Here's the User bean:

<?php
// User.php

namespace dac\silexdemo\beans;

class User {

    protected $id;
    protected $firstName;
    protected $lastName;

    function __construct($id, $firstName, $lastName){
        $this->id = $id;
        $this->firstName = $firstName;
        $this->lastName = $lastName;
    }

    function getId(){
        return $this->id;
    }

    function getFirstName(){
        return $this->firstName;
    }

    function getLastName(){
        return $this->lastName;
    }

}

Note there's nothing in there to help the factory process: it's just a standard class.

That gets passed to the Twig file, which is then rendered:

{# user.html.twig #}

ID: {{ user.id }}<br>
First Name: {{ user.firstName }}<br>
Last Name: {{ user.lastName }}<br>

Note how I don't need to call the getter methods explicitly: Twig works that out for me. Because I'm accessing user.id, it knows to get that with getId(). Cool!

And all this renders the - very spectacular - result of:

ID: 1
First Name: Zachary
Last Name: Cameron Lynch


Job done.

That's about it, really. I've put Silex through its motions, testing its routing and its integration with Pimple. I've had a look at Guzzle, and rendered stuff with Twig.

From start to finish - including research and doc-reading time - this took about eight hours I guess. 4h the other day to do the installs and get the routing sorted out; another 4h today for the DI, controllers, model (including Guzzle) and view stuff. And maybe 2h to write up and proofread. Pretty quick. This is not a brag (at all), it demonstrates that everything is documented well, googling for stuff is easy with PHP because the community is so big, and that the apps actually do what they set out to do, and just get on with it, rather than throwing unnecessary shite at me (like some of my experiences with popular CFML solutions to similar situations).

And my flight's finally in the air, so I'm gonna shut this thing down and do something more entertaining. Sleep, probably.

Righto.

--
Adam