Category Archives: Blog

Route to madness: Too much magic in Laravel!

Some people say that Laravel’s facades are optional, and if you don’t want to use them, then you can use dependency injection or the helper methods instead.

Laravel does have a powerful DI container. I have tried to go as facade-less as I can, but again I’ve found routing to be a pain.

Using some of PHP’s magic methods, Laravel likes to proxy calls. A lot. The obvious examples are, of course, Facades. When you call something like Cache::get('something'), Laravel proxies that call into the container and whatever service is required. There’s even an explanation in the official documentation.

One of the problems with proxying calls like this is that it can become a little too magical. Out of the box, a call like this cannot be analysed statically (that is to say, without running the code), to determine what’s going on.

The solution to this is to provide hints for an IDE to pick up on. There’s a package for this which outputs an _ide_helper.php file containing hundreds of automatically generated hints.

Recently, I wanted to see if it was possible to ditch facades in the routes file. The method that loads routes adds a $router variable when it includes the routes file, so it seems like Laravel is indicating you can:

//Illuminate\Routing\Router
protected function loadRoutes($routes)
{
    if ($routes instanceof Closure) {
        $routes($this);
    } else {
        $router = $this;

        require $routes;
    }
}

And a cursory test proves this to work.

// Works
Route::get('/', function () {
    return 'Hello World';
});

// Also works
$router->get('/', function () {
    return 'Hello World';
});

Other method calls also work:

Route::view('/', 'welcome');

$router->view('/', 'Welcome');

We can also use closures for things like groups:

Route::middleware([TrimStrings::class])->group(function () {
    Route::get('/', function () {
        return '/';
    });
});

$router->middleware([TrimStrings::class])->group(function () use ($router) {
    $router->get('/', function () {
        return '/';
    });
});

The only difference to the code is that we have to pass the $router variable into the closure because it’s otherwise not in scope.

tl;dr – replace Route:: with $router-> and remember to pass the $router variable into closures. Simple. End of blog post right? No.

Consider Route Prefixes, the example in the documentation looks like this:

Route::prefix('admin')->group(function () {
    Route::get('users', function () {
        return '/admin/users';
    });
});

Following the previous facade removals, we should be able to change this to:

$router->prefix('admin')->group(function () use ($router) {
    $router->get('users', function () {
        return '/admin/users';
    });
});

But when we try this, the first example works, but the Facade-less version breaks:

Wtf why?

No, this isn’t a typo or other trivial bug in my code. It’s a deeper issue with how Laravel proxies calls all over the place.

Let’s first look at the Facade version and follow it.

The Route facade extends an abstract facade class which implements __callStatic, passing in the method name ('proxy') and the arguments passed in (the closure):

public static function __callStatic($method, $args)
{
    $instance = static::getFacadeRoot();

    if (! $instance) {
        throw new RuntimeException('A facade root has not been set.');
    }

    return $instance->$method(...$args);
}

The call to getFacadeRoot essentially just returns $app['route'] – or whatever key the facade says to use.

So when we call Route::prefix, Laravel resolves the Router from its container, then invokes the prefix method on it.

If we look at the Router class, does it have a public prefix method? No. Public is important, because if the method exists but it’s protected or private, then it cannot be called and instead PHP try __call instead.

We can demonstrate this easily:

$test = new class
{
    protected function foo(): void
    {
        print 'foo';
    }

    public function __call($name, $arguments)
    {
        print 'calling magic method!';
    }
};

$test->foo();
$ php test.php
calling magic method!

Instead of calling the foo method which does exist, PHP calls __call because the foo method is not public.

The Router does have a protected prefix method. So why is it called when using $router but not when using a facade?

Answer: Because $router is actually an alias of $this.

Cast your memory back to when the routes file is included:

//Illuminate\Routing\Router
protected function loadRoutes($routes)
{
    if ($routes instanceof Closure) {
        $routes($this);
    } else {
        $router = $this;

        require $routes;
    }
}

Which means the in the facade-less code, you can replace $router with $this and it’s functionally the same:

$router->prefix('admin')->group(function () use ($router) {
    $router->get('users', function () {
        return '/admin/users';
    });
});

$this->prefix('admin')->group(function () {
    $this->get('users', function () {
        // Matches The "/admin/users" URL
        return '/admin/users';
    });
});

Of course $this can call its own protected method, so it does, and breaks, because while the facade version will correctly call Router::__call, and then filter its way into RouteRegistrar, the facade-less version will instead call Route::prefix and break.

What a mess.

The workaround to this, which I eventually ditched in favour of returning to facades, is to explicitly call __call and wrap the arguments with an array

Which means these three approaches are all equivalent:

Route::prefix('admin')->group(function () {
    Route::get('users', function () {
        return '/admin/users';
    });
});

$router->__call('prefix', ['admin'])->group(function () use ($router) {
    $router->get('users', function () {
        return '/admin/users';
    });
});

$this->__call('prefix', ['admin'])->group(function () {
    $this->get('users', function () {
        return '/admin/users';
    });
});

In the end, I ended up going back to using facades for routing.

Multiple Subdomain and TLD Routing in Laravel 5

A recent project I’ve been working on has a requirement to have two concurrent versions of an application running on different subdomains.

It looks something like:

  • sandbox.example.com – A sandbox version of the application
  • live.example.com – The live version

The local developer version has the same URLs, just with a .test or .local TLD.

Laravel 5 supports subdomain routing, but not really for this use case, and honestly, not all that well out of the box.

This is the example found in the documentation:

Route::domain('{account}.myapp.com')->group(function () {
    Route::get('user/{id}', function ($account, $id) {
        //
    });
});

Problem 1: This route is now hardcoded to the .com TLD.

You can’t develop locally any more because your local environment will be probably on a .test TLD or another one of the reserved testing TLDs.

You might think that Laravel could intelligently swap out the TLD when it’s being ran in development mode, but no such feature exists. The code example from the docs cannot be copy and pasted into a real project and work as expected.

That said, one possible work around for this is to put the TLD in an environment variable that can be swapped out at run time.

Route::domain('{account}.myapp.' . config('app.tld'))->group(function () {
    Route::get('user/{id}', function ($account, $id) {
        //
    });
});

Problem 2: This route now requires the {account} parameter.

Let’s name this route and try to use generate a link with the route helper.

Route::domain('{account}.myapp.' . config('app.tld'))->group(function () {
    Route::get('user/{id}', function ($account, $id) {
        //
    })->name('account.profile');
});
// Illuminate \ Routing \ Exceptions \ UrlGenerationException
// Missing required parameters for [Route: account.profile] [URI: user/{id}].
return route('account.profile', ['id' => 1234]);

The subdomain parameter is required but not provided, so UrlGenerationException is thrown. Also note that Laravel doesn’t tell you which parameter is missing which is not very helpful!

Obviously you can fix this by providing the subdomain in every call to route, but this has obvious problems:

  • All controller methods will need to accept the subdomain as a parameter and pass it along to the view.
  • All calls to the route helper will need to specify the subdomain

This perhaps makes sense if you are basing application logic on different subdomains, but in my case I’m not.

The solution

In your EventsServiceProvider, listen for the RouteMatched Event. When it fires, Laravel will have matched the route to use and will populate the current route with the parameters from the request.

You can inject these into the UrlGenerator‘s default values.

From this point on, the parameters you specified in the domain of your route will be filled with whatever matched them.

//EventsServiceProvider.php
protected $listen = [
   RouteMatched::class => [
       SetUrlDefaults::class,
   ],
];
//SetUrlDefaults.php
use Illuminate\Routing\Router;
use Illuminate\Routing\UrlGenerator;

class SetUrlDefaults
{
    private $urlGenerator;
    private $router;

    public function __construct(UrlGenerator $urlGenerator, Router $router)
    {
        $this->urlGenerator = $urlGenerator;
        $this->router       = $router;
    }

    public function handle(): void
    {
        $this->urlGenerator->defaults($this->router->current()->parameters);
    }
}

You could also use the URL and Router facades here if you really want. I prefer dependency injection.

Conclusion

Maybe all this seems obvious in hindsight, but figuring this out took me several hours and many missteps in the wrong direction.

It’s disappointing to me that examples in the official documentation have implementation issues that require either a good amount of experience of the framework or more advanced code splunking abilities.  I would put my solution and debugging above what I would expect from a junior or even mid tier developer.

Stride and the ninety-nintey rule

This was written before the partnership between Atlassian and Slack was announced, discontinuing Stride after only less than a year of it being released.

In the development world, there is a saying:

The first 90 percent of the code accounts for the first 90 percent of the development time. The remaining 10 percent of the code accounts for the other 90 percent of the development time

— Tom Cargill, Bell Labs

Case in point, Stride. The new-ish Slack competitor from Atlassian and replacement to Atlassian’s much neglected HipChat.

Stride is 90% there and it is a massive step up from HipChat, but that’s not saying much considering that HipChat was only marginally better than tin cans and string.

I want to go over some of the reasons why I don’t like Stride. It is an interesting case, because it’s all the little things that are hard to pinpoint that make Stride so much inferior to Slack but hard to tell why.

Too much whitespace

The biggest problem is the overuse of whitespace.

Compare Stride:And Slack:Stride has four columns, whereas Slack has just three (or two if you only belong to a single organisation).

Stride has so much whitespace between everything. The chat pane even has a copious amount of padding either side of the text.

Look at the amount of dead space there is on the far right and left columns in Stride. The right panel is especially egregious because it contains the online users indicator, files, highlights, meetings, apps, and app icons. Really all of that could be moved into the top panel or into a menu (like Slack does). All of this stuff is used so little it does not deserve an entire column that can’t be collapsed or shrank.

Look at the avatars – despite the excessive padding, they are smaller in Stride

There is also a forced circular crop applied to them which manages to mangle every single avatar upload to it in our particular instance. Nobody has an avatar that looks better in a circle. Nobody.

Look at the space between individual messages. Slack is noticeably more compact, and it’s not even in its compact setting.

Stride has these panel things that notify you of changes from JIRA, etc, or when meetings start. Again, they are simply too big.

Just moving an issue about or changing its status easily fills up a the entire vertical space of the window. There’s no need for the extra line between the title and the info at the bottom. There’s no need for it to be wrapped in a grey box. Also of note, this screenshot shows a bug where the avatar for me next to Assignee is missing, despite me having an avatar in both JIRA and Stride.

Look at the sheer amount of space between rooms and people. In the space Stride takes to list 4 rooms, Slack can list about 7. Another thing to see here are the icons next to rooms. They are too small to tell apart, but take up too much space. The worst of both worlds. You also cannot customise them much. You can only pick out from premade icons, and there seems to be no way to get that pretty rainbow effect in reality.

Missing features

1. Stride does not let you reply to messages with an emoji like Slack or Discord.

This has become an invaluable feature for me in Slack. It lets you quickly and silently acknowledge something. And often gets used for great humour…

Meanwhile, Stride does have Actions and Decisions. Two features I have literally never used.

2. Stride does not provide typing indicators either for chat rooms or PMs.

This is really useful when you are chatting in a busy room, or when something big is happening. It’s a strange omission.

3. Dark mode

Slack doesn’t have this, but HipChat did. Stride is missing it too. Given that Stride is a forced upgrade from HipChat in organisations that upgrade, its omission is shocking.

I went to WordCamp London 2018

Between the upcoming VuTales rewrite, some freelance/ consultancy projects, and my love/hate feelings towards WordPress in general – I ventured (alone!) to London to attend WordCamp Lodon this year.

In numbers: this was the fifth PHP conference I’ve been to, the second I’ve personally paid for, and my first WordCamp. Now I think I finally have enough experience of these things to really compare and contrast them and give a proper review.

I’ve found conferences to be a mixed bag overall and feel the same about WordCamp. Overall though I enjoyed my time there like I have at all the conferences I’ve been to, and I’ll attend next year and maybe some other regionals. Maybe Brighton – I’m not sure.

The talks

When I say that I’ve found conferences to be a mixed bag, what I really mean are the talks.

I contend that the best talk I’ve ever seen (in person too!) was Michael Cullum‘s talk at PHPNW 2015: phpBB, Meet Symfony. The reason is because it was a talk that you couldn’t really condense down into prose and make into a blog entry. It was entertaining but also informative and told a story that worked best as a talk.

Conversely, I’ve attended talks where people have literally copied a top 10 list from their website/blog and converted it into a talk. These kinds of talks tend to be terrible.

Although a lot of talks could probably be blog articles, the better ones present something difficult and use the medium of a presentation to explain things that are hard to express or explain in prose. And to that, one or two of talks I attended I felt were too remedial. To be clear, this is a gripe with all conferences I’ve been to, not just WordCamp. That said, it’s a balancing act – and what I consider simple as a senior developer others may not.

David Lockie‘s talk on Blockchains was interesting. A shame it ran out of time towards the end. I am very dubious towards the abundance of blockchain related tech, but the talk was a good starting point for some of the use cases that are not cryptocurrency. I do think that there wasn’t enough linking back to WordPress in the talk – especially given that the title started with “WordPress and Blockchain”.

Tammie Listers‘ talk on Gutenberg was a good jump into the subject especially for a back end dev like myself.

Unfortunately there were two talks I attended that I felt were poor.

The first was a talk on an idea that I felt was very technically questionable. It was a solution looking for a problem that was over-engineered, brittle, and had significantly better and easier options other than the one being proposed.

The second was apparently showcasing a product that was meant to be immediately available but as of writing isn’t (also the website is still broken). It also advocated using WordPress as a development framework. WordPress is not a development framework, it’s a CMS . Do not try to use it as a framework. You will write terrible and inflexible code. There are much, much better alternatives for that (Lavavel, Slim + components, etc).

Things I liked

The ticket price – At £40 it was a steal. Under half the next cheapest ticket (£92 for PHP South Coast’s Early Bird ticket). One of the key reasons I decided to go was because it would be so affordable. The biggest expenditure was the hotel costing something like 2/3s of the overall cost for me.

The venue – Having been to now PHP South Coast and PHP North West – the latter in a dedicated conference location – I was shocked at how good the venue was. By far the nicest I’ve been too. The aircon worked. The lecture rooms were all well sized and only once did I see one end up standing room only.

The catering was the well above what I was expecting and organised with military precision and efficiency. Between talks there was free tea, coffee, apple and orange juice as well as biscuits and fruit.

There were security guards haunting the entrances to the buildings, which felt a bit odd – but whatever.

Sponsors – sponsors at conferences are what pays for stuff, but WordCamp despite the low ticket cost didn’t repeatedly bash me over the head with announcements from sponsors and overbearing calls to go visit their stands. As before, very impressed given the low ticket price.

There was one sponsor whose stand I refused to visit because I find their advertising to be repugnant.

I managed to haul a nice water bottle, a 32GB USB stick, two notepads and a good quality (if small!) mug all from Timpani. As well as a fair amount of stickers, pens, and assorted whatnot from everyone else. I now have WordPress and Jetpack badges on my backpack and stuck a few of the stickers on my MBP.

Things I didn’t like

Premier Inn – Why must you be so expensive and the rooms always so hot!? Argh!