You’ve Got To Go Forwards To Go Back

Another week goes by, and another newsletter where I can’t announce the launch of the revised version of CodeReviewVideos. It’s killing me.

I’ve pulled a PHP 6.

I’m now onto CodeReviewVideos version 3, and yet version 2 never got released! Oh my.

What’s most amusing to me is that v3 is so similar to v1 in terms of look-and-feel that I almost needn’t have bothered. Good times. Good times.

Anyway, I still have an absolute ton of stuff left to do, so today’s update is going to be short and sweet.

Video Update

This week saw three new videos added to the site:

#1 – Docker PHP Symfony Tutorial

In order to get a Symfony stack up and running with Docker there are a few pieces that already need to be in place.

These are your webserver (nginx, in our case), and likely a database (mysql, for us).

Our web stack

Also, being that Symfony uses PHP, we kinda need PHP available inside the Docker container 🙂

Now the good news is that we sorted out PHP last week. We created a ‘base’ image, based on PHP 7.1.

We can now use as the starting point for any PHP project we do. This could be Symfony, or WordPress, or Slim, or whatever.

In our case this will be a Symfony Docker image.

And what this means is that we will do a bunch of Symfony-specific tasks such as copying over the app , src , web , and bin  directories, and setting up the parameters.yml  file.

By the end of this video you will have all the pieces of the puzzle (the puzzle being: how to get a Symfony stack up and running in Docker) ready to go.

You could – if you were being particularly cruel on yourself – decide that you’re going to docker run all these images and get everything hooked up by hand.

Or, if you prefer life being that little bit easier then you could use Docker Compose to make this task massively simpler…

#2 – Docker Compose Tutorial

In a nutshell, Docker Compose allows you to define an environment in which your project / code will run.

As above, in our case we need nginx for our web server, we need MySQL as our database server, and we need our Symfony code to run our application.

Docker Compose allows us to define how all of this fits together.

It might not make much sense as words, so here’s some real config:

version: '3'

services:

    db:
        image: mysql:5.7.19
        hostname: db
        volumes:
          - "./volumes/mysql_dev:/var/lib/mysql"
        env_file:
          - ./.env

    nginx:
        image: docker.io/codereviewvideos/nginx.symfony.dev
        hostname: nginx
        volumes:
          - "./volumes/nginx/logs:/var/log/nginx/"
          - "./:/var/www/dev"
        ports:
          - 81:80
        depends_on:
          - php

    php:
        image: docker.io/codereviewvideos/symfony.dev
        hostname: php
        volumes:
          - "./volumes/php/var/cache:/var/www/dev/var/cache/:rw"
          - "./volumes/php/var/sessions:/var/www/dev/var/sessions/:rw"
          - "./volumes/php/var/logs:/var/www/dev/var/logs/:rw"
          - "./:/var/www/dev"
        env_file:
          - ./.env
        depends_on:
          - db

Even if you don’t understand how it all works, the likelihood is you can figure out what it will do if you were to run docker-compose up  against this config.

However, one of my goals with CodeReviewVideos is to ensure you do understand how. And so in this video we cover exactly that.

#3 – Docker Compose Tutorial For Elixir and Phoenix Framework

In this tutorial series we are covering how to get a working Phoenix Framework environment up and running with Docker.

In case you aren’t aware, Phoenix is a web framework written in Elixir, a rather interesting functional programming language.

The main reason for this tutorial series is to show you that the back end and front end code can be completely separate entities.

We could use Symfony as our back end. We could use Laravel. We could use Django, or Rails, or in this case, Phoenix.

Ultimately, in most of my projects lately, all my back end does is serve up a JSON API.

From the point of view of the front end (React, Angular, Vue, some mobile app, or whatever), it doesn’t matter – at all – how the back end works. It just needs to work.

One of the often repeated pieces of advice for programmers who want to improve their craft is to “learn a different language”.

We aren’t going to be learning Elixir here. If there is interest then I would be happy to share what I know.

However this series is intended to show you how to achieve a working JSON API, and quickly, in a different environment to that which you might be used to.

Hopefully you find it interesting regardless of whether you decide to investigate Elixir / Phoenix any further.

And that’s about it for me this week.

I’ve had a lot of requests lately for “how to deploy Symfony“, so this will be covered in next week’s videos.

Until next week, have a great weekend and happy coding.

Chris

PHP > React > Server Side Rendering > PHP

Back when I was a server room techy there was a line my old boss used to say whenever I got wind of something new and shiny:

There are no new ideas in IT, just the same ones on repeat.

Of course he was being tongue-in-cheek, but there was a truth in what he said.

For example, around the time I was in this role, the IT industry was moving from distributed to centralised. That is to say we had a bunch of servers dotted all over the county, and the plan was to bring them into just two central server rooms.

This, I was assured, was very much like “the olden days” when individual desktops were replaced with “dumb terminals”, which relied heavily on a centralised Mainframe.

At some point, someone (likely an army of well paid consultants) espoused the failings of such an architecture (oh my, single point of failure!) and likely sold them a bunch of high powered standalone desktops.

This worked well for a while, then companies like Citrix came along and sold a different spin on “dumb terminals” using your existing high powered desktop, and so on, and so on.

What the heck does any of this have to do with Web Development, I hear you ask.

Good question.

As you may recall, I have been getting quite excited about launching the new, shiny revision of CodeReviewVideos.com. I had my zip lock baggy of party poppers at the ready. Things were looking super.

Then, over the previous weekend, it dawned on me:

How’s the SEO on this new site then?

Seeing as about 70% of the incoming visitors to CodeReviewVideos arrive via Google, I figured I should probably – ya’ know – give this some consideration.

I checked, and it turned out that I had, ahem, neglected to set any of the head information.

Yes, I felt quite the chimp.

But not to worry, I have all the SEO data just sat ready and waiting in the DB. After all, it’s exactly the same as for the existing site.

What happens with the existing site is what happens with pretty much any PHP site I have ever worked with:

  • A request comes in
  • The relevant data is fetched from the DB (thanks, Doctrine)
  • This data populates a template (thanks, Twig)
  • The response is returned to the end user (thanks, Symfony!)

It doesn’t matter if the request is from Google Bot, or from a real person. The process is always the same.

What this means is that the response contains everything needed to make a full page representation.

Google Bot can look at the page and see all the expected “stuff”: the header tags, and body content, it’s all there.

Sure, we can then augment this with a snazzy bit of JS here and there, but largely, it’s good to go.

Web 2 Point… Oh?!

Then sometime a few years back, Single Page Applications (SPA, and not the relaxing kind) became popular.

SPA – not as relaxing as you may have been led to believe

Fast forward a few years and boom, I’ve done a few of these here SPA’s with React and Angular, and I’m thinking: yeah, this is awesome, let’s make CodeReviewVideos all snazzy using all this wicked tech.

So I did.

The new version of the site uses React, and Redux, and Redux Saga, and it talks to a Symfony JSON API, and it’s all lovingly tested and fills me with warm fuzzy feels whenever I work with either code base.

Unfortunately, all this awesomeness does make the architecture more complex. Let’s revisit how a request / response works now:

  • A request comes in
  • The user gets sent this:
<!DOCTYPE html><html lang="en"><head><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"><title>Code Review Videos</title><script type="text/javascript" src="https://js.stripe.com/v2/"></script><link rel="shortcut icon" href="/favicon.ico"><link href="/main.0c31c168e455a7de8ecee02d7d21798e.css" rel="stylesheet"></head><body><div id="app"></div><script type="text/javascript" src="/main.bf614c9bf38e83f9d4e0.js"></script></body></html>
  • We are done, let’s go to the pub.

If you’re a bot Yahoo, Bing, or Yandex, you’re now done.

Forgetaboutit

By all accounts these bots do not process JavaScript, so forgettaboutit.

This may very well be of no major consequence.

By way of some hard figures, the combined total visitors that CodeReviewVideos received over the last 7 days that came via a search engine that isn’t Google was:

11.

Fixing the world for the sake of these other search engines probably isn’t worth my time right now.

But Google… well, we must do better.

Now, it turns out that maybe Google has already solved this problem.

My guess is they have. I mean, this is Google. They are super smart, and it’s their job to get this right.

But can I risk killing the site on a maybe.

No, definitely not.

What Happens Next?

Back to the HTML from above. Thinking about the request / response sequence, what happens next?

Well, on the client side – which is to say in your visitor’s browser – assuming they have JavaScript enabled, the JS is read and run (or parsed and executed, if you’re a CS text book).

Cus i’m Shiny!

This is where all the cool stuff happens!

  • React builds the page from my components
  • The components instruct the browser to make requests to the back-end API for the real content
  • The real content is received
  • React renders the content into the right place(s)
  • The visitor is completely indifferent

By the way, if the visitor doesn’t have JavaScript enabled then they are going to have a bad time. But, ya know, 2017, etc, etc.

When I realised my mistake about SEO last weekend, my first thought was to simply grab my Helmet and populate the head tags using the API response.

Alas, as we have now seen, this simply will not do.

So I figured I would be a Smart Guy ™ and put a caching layer in front of my web server. Maybe I could trick the bots by returning a fully cached version of each page.

Yeah, that didn’t work.

At this point I had a short breakdown.

Fortunately, PHP North West happened which took my mind off of the problem.

Ultimately though, it meant I had to postpone the scheduled launch. And that sucks.

Server Side Rendering anybody? No? SSR? No?

Of course I’m not the first person to have experienced this problem.

There is already a solution:

Server Side Rendering.

The idea here is rather convoluted, but stick with me:

We already have our shiny new React site, right? Yes, yes we do.

Server-side-rendiamous!

So, let’s get a Node JS instance to sit in front of our React site, and uses some of the stuff we learned in the Dark Arts class at Hogwarts to run this code, then convert the response to a string, and write the string to a template, which is turned into another string which is sent back to the browser so that we now have the necessary HTML to please Google like in the olden days.

Oh my.

It seems like we’re repeating ourselves here.

But now with more layers. More bug filled, confusing, maybe unnecessary layers.

Anyway, that’s where I’m at with CodeReviewVideos right now. Trying to migrate my existing front end code from client-side only, to this SSR setup to essentially reproduce the outcome I already have with plain old PHP.

What this means is right now I have no ETA on the next version, and once it is again “ready”, I will need to go back through a phase of testing before go-live.

Video Update

This week saw three new videos added to the site:

#1 – [Part 1] – Twig Extensions – How to add a custom CSS class to an invalid form field

Between this, and the next video we look at a question I got asked late last week:

How can I add a custom CSS class to the `input` / other form field when validation fails for that field?

This video explores one possible solution to this problem.

What happens however, is that we end up with a bunch of repeated logic living in a Twig template.

Generally, if you find yourself repeating yourself… or you end up with complex logic living inside a Twig template, you can probably extract it into a better location – a custom Twig function.

#2 – [Part 2] – Twig Extensions – Create a Twig Extension Function to Keep DRY

As such we look at extracting this repeated logic from our Twig template into a Twig Extension.

It’s one of those topics that sounds like it might be hard, or too nerdy, or whatever.

It’s just a strange name for a very useful concept.

If you use Twig in any seriousness, and you don’t yet know about Twig Extensions then this video should help you understand how to use one, and why it’s not as hard as you might think to do so.

#3 – Docker Elixir Phoenix – Part 1 – The Web Server

In my personal experience there has been no better way to improve my overall knowledge of software development than by learning another language.

Elixir, in this case, is the language.

Phoenix is somewhat akin to Symfony, or Rails, or what have you. It’s a framework for web applications.

In this series we are building up a small but functional (ho, ho) JSON API using Elixir and Phoenix.

In this video we cover how to set up the Web server by way of docker-compose.yml. This series is a little more advanced than most of the content on CodeReviewVideos, but I hope you’re finding it enjoyable all the same.

That’s It, That’s All

That’s just about it from me this week.

My favourite snowboarding movie 🙂 Great soundtrack.

As mentioned last week I spent the previous weekend at the PHP North West conference.

You can read about my time there right here.

There are pictures 🙂 and also a lesser spotted Rasmus found in his natural habitat.

Have a great weekend, and happy coding.

Chris

Beta Blockers

It’s been a busy week here at CodeReviewVideos.

I want to thank everyone who tried out the site in the “beta” test. This has now ended, and I’m ready to launch the new site version. Hooray.

The only reason to delay now is that I’m away all weekend (more on this below), and as such, don’t want to launch on a Friday. Instead, the launch will likely be Tuesday, all things being equal.

There’s a bunch of tasks for the launch – it’s not quite as straightforward as I’d like. The hardest part is around migrating the database. I’ve changed the structure quite a lot between versions, and keeping both in sync across two different servers is proving… interesting.

There have been so many lessons learned during this process. Just this week I realised none of the pages had HTML head tags. Whoops. But then it turns out populating these tags from an API response isn’t super straightforward either. There’s no server-side rendering to help me 🙁

Another problem that caught me totally off guard was the sitemap.

Being that CodeReviewVideos is an existing, live site I know that whatever SEO ‘stuff’ I have in place already must be mirrored to the new site.

What I hadn’t thought through was the location of the sitemap.xml  file.

In the current iteration, I’m using Symfony with Twig. Everything lives on one server, and as such, making the sitemap is really straightforward. I use a bundle (PrestaSitemapBundle) and it just works.

For the new site there are two areas where pages may come from:

  • Static pages – i.e. the FAQ, contact / support, privacy policy, etc
  • Dynamic pages – i.e. the Course / Episode pages

There are some libraries available that will help generate sitemaps for React-based projects. I looked at each in turn but none seemed to fit my needs. I found this odd, honestly, as surely I’m not the first person to meet this problem.

Instead, I decided to roll my own. Well, I say “roll my own”, but really it’s a combination of some Doctrine query output dumped into Samdark/sitemap.

This worked really well, but.

It’s a big but.

The sitemap is generated on a sub-domain – under api dot codereviewvideos dot com.

Apparently, having done some research, Google ain’t so keen on this. They want the sitemap to live on www dot codereviewvideos dot com.

The problem being that behind the scenes these are two entirely separate concepts. Both are Docker images with all the necessary data “baked in”. How could I dynamically generate a sitemap from one, and it appear on the other?

Well, it turned out Docker Volumes came to my rescue. I could bind mount a file on the underlying host, and make the file appear as though it were on the www site automatically.

There have been an absolute ton of unexpected surprises like this along the way.

Video Update

This week saw three new videos added to the site.

#1 – Docker MySQL Tutorial

There’s a school of thought that says Docker and Databases don’t make good bedfellows.

For my money, this debate is really only worth having if you’re heading into production. If you’re simply wanting to play around with a database, or you have some local development work to get done, then Docker is perfect for the task.

In this video you will learn how to quickly spin up a working MySQL database server, in almost no time at all. Once you’ve got the latest MySQL Docker image stored locally on your computer, getting a new container up and running takes seconds.

As with almost anything Docker related, there are a few potential gotchas. It’s really all about reading the instructions (or the docker logs  output) if things don’t go according to plan. In particular here, you will need to use one of the mandatory environment variables.

We finish up this video by taking the example docker run command provided in the docs, and turning it into a more practical, usable variant along with storing our database on a named volume.

#2 – Docker nginx PHP Tutorial

Getting MySQL up and running was fairly easy going. And earlier in this series we saw how we could quickly spin up a Dockerised nginx server without much effort, either.

However, beyond some simple examples, the truth is you will need to do quite a bit more to get a working development environment that replicates Vagrant or some Ansible-managed VM infrastructure.

The way we will work through this is to separate nginx from the actual application code. In other words, nginx, php, and our Symfony site will all be separate Docker images.

On the surface of it, this seems like we’re going a little overkill. However, stick with me through this, and the next two videos, as this approach has served me well in the real world.

#3 – Docker PHP 7 Tutorial (7, 7.1, and higher)

As covered in the previous Docker nginx tutorial, our stack will be split across multiple Docker images and containers.

We’re going to let nginx accept incoming requests for our PHP-powered site(s), but the nginx container won’t know how to process PHP code. Instead, it will hand off to a dedicated PHP-processing container.

For our purposes we are going to create a PHP 7.1 image. This will be relatively generic, containing all the stuff most every PHP site will need. This will include a bunch of system level dependencies such as curl , and git , through to PHP deps such as intl , pdo_mysql , and optionally xdebug .

However, we won’t directly use the PHP image we create. Instead we will use this as the starting point for all future PHP Docker images. For example, in the next video we will use it as the basis of a Symfony image. But I also use the image as the base for WordPress, and custom PHP projects too.

PHP North West

This weekend I will be at PHP North West. If you are heading to PHP North West then please say hello 🙂 I already know a few of you will be attending, and I’m looking forward to meeting up in person.

The other reason I mention this is that support response times may be a little slower than usual. Apologies in advance if this causes you any inconvenience.

This will be my fourth year attending PHP North West, and whilst I didn’t blog about my first visit (not sure why), I did capture the previous two. Expect similar for PHPNW17 some time next week.

 

Pffft, who browses on Mobile anyway?

Several weeks ago I decided to knuckle down and get the new version of CodeReviewVideos.com out from the safe confines of my desktop, to the scary world of Production.

“How much work can it be?”, I thought, rather foolishly.

It turns out that I had completely underestimated the scale of the task.

As an indicator of just how poor my estimation was, I thought it would take ~2 days. It has taken 3 weeks 🙂

But the good news is: it’s done!

Woohoo.

Well, of course, I say done, but what I really mean is it’s ready to launch but there’s still a whole raft of improvements yet to come.

But we already did it. It took SEVEN HOURS, but we did it. It’s done.

Ok, so to set the scene:

My vision for the next major release was to separate the front end and the back end entirely. This means the front end code (React/Redux) lives in one GitLab repo, and the back end (Symfony) lives in a different GitLab repo. The only way they interact is via HTTP requests.

This has worked really well, but came with a whole bunch of ‘gotchas’ that I hadn’t planned for.

There’s still a big one on my todo list: the Sitemap.

For CodeReviewVideos when using Symfony with Twig I made use of Presta SiteMap Bundle. This works really well. It does the job it sets out to do, and generally gets out of the way.

However, being that the back end is now simply a JSON API, I cannot use Presta SiteMap Bundle any longer – after all, Symfony is no longer aware of every available route.

It’s not a show stopper, but it does delay me switching the site over completely until I figure it out.

Then there’s the Mobile Responsive view.

Even though it’s 2017, and one of the first things I do when I wake up in the morning is to roll over and check my phone, I still routinely forget to test my dev projects outside of Desktop view.

As such, it only dawned on me on Tuesday this week that hey, perhaps I better check what the site looks like for mobile. And needless to say, it didn’t look good 🙂

There have been a bunch of other issues, particularly with Bootstrap / CSS. I decided to go with Bootstrap 4 for this one – figuring it would be relatively easy to do large chunks of copy / paste from the existing site to the new one.

Again, not so.

Whenever I use an image in a video write-up I use the css class of img-responsive which means, as you likely will have guessed, the image scales nicely depending on the screen size.

Oh sure, I meant for it to look this way :/

In BS4 they have removed img-responsive. Now it’s img-fluid. Even though it sounds minor, it still involves a bunch of MySQL tomfoolery to find and update each instance, and that all costs time.

Anyway, I could go on, and on… and on about this but that wouldn’t be much fun for anyone, I’m sure.

I really just wanted to share this as I’ve had conversations with other fellow developers in the past and have found sometimes they believe it’s a problem personal to them. Like they should have anticipated every potential problem and planned accordingly. Launching stuff is hard 🙂

Video Update

It wasn’t all work on the new site this week. Heck no.

I managed to get three new videos up, too.

#1 – Symfony Event Subscriber Tutorial

Are you using Events in your code?

If not, you may be missing out on a way to separate and (from one perspective) simplify your code.

I use Events all the time.

In this video we look at Symfony’s Event Subscribers, and how they can be used to split up your logic into easily digestible chunks.

And that’s all great, but sometimes a real world example can illustrate the point a lot better, so…

#2 – Symfony Event Subscriber in a JSON API Example

This video covers one way I’m using custom Events and Event Subscribers to respond to ‘things’ (some may call them: Events) that happen in my code.

For the purposes of this example I want to show how I use an Event Subscriber to help process incoming POST  requests when using Symfony as a JSON API.

In just over three minutes, the aim of this video is to show how easy this setup is to use inside a real code base. I say real, because aside from changing some variable / object names, this code is ripped directly from the new version of CodeReviewVideos 🙂

#3 – Symfony Events – The Gotchas

Alas, using Events in Symfony isn’t all happy paths and hi-5s. There are some potential gotchas.

In this final video in this short series we cover some – potentially – unintuitive ways that Events behave inside Symfony. This stuff may be obvious to you, but all of these problems are things I have experienced as points of pain for other fellow devs in real projects.

Watch this video and keep your blood pressure at its normal level 🙂

Other Stuff

If you haven’t received an invite to test out the new site, and you’d like one, please do get in touch.

Please don’t take it personally if you haven’t received an invite – I’ve been basing it on the most recently logged in users during the exact times I’ve been sending out invites, and only in groups of twos and threes.

PHP North West is next weekend.

If you are planning on being there, I’d love to meet up and say “hi” in person. Please get in touch if so.

Ok, until next week, have a great weekend and happy coding.

Chris

Go, No Go For Launch

Behind the scenes this week I have been extremely busy with the new site launch.

All being well, I anticipate sending out the first batch of emails today. If you’re an active site subscriber, please keep an eye on your inbox.

I still have a bunch of outstanding tasks before then, so I’m going to keep today’s update short and sweet.

Whilst I have your attention would you mind hitting reply and letting me know:

If you could see a video / course on the site that would help you right now, what would it be about?

Thanks!

This week saw three new videos added to the site:

#1 – Dependency Injection and Symfony Services (Updated for 3.3)

First up we are covering the recent changes to Symfony’s approach to Dependency Injection.

One of the recurring issues I see with many developers who are new to Symfony is in understanding how to access Services from other Services.

See, it’s fairly obvious in a Controller. There are a lot of examples about, and particularly in the docs this use case is covered a lot.

However, after a short length of time in using Symfony you will inevitably migrate from everything in your controller, to controllers making use of services.

This means creating your own services.

And these services need access to other services – whether they are services provided by the Symfony framework, or ones of your own / your team’s creation.

I often see the same mistakes being made here, so wanted to cover this in a video I can refer developers to in the future.

#2 – An Introduction to Symfony Events

I make a heck of a lot of use of Symfony’s event dispatcher.

Typically these days I try to keep my controller actions as small as possible, and one way to do this is to dispatch events when things happen during a controller action.

This is better illustrated with some code, so here goes:

    public function postAction(
        Request $request,
        EntityManagerInterface $em,
        EventDispatcher $eventDispatcher,
        CourseFactory $courseFactory
    )
    {
        $form = $this->createForm(CourseType::class, $courseFactory->create(), [
            'csrf_protection' => false,
        ]);

        $form->submit($request->request->all());

        if (!$form->isValid()) {
            return $form;
        }

        $course = $form->getData();

        $em->persist($course);
        $em->flush();

        $eventDispatcher->dispatch(
            Events::COURSE_CREATED,
            new CourseCreatedEvent($course)
        );

        $routeOptions = [
            'slug'    => $course->getSlug(),
            '_format' => $request->get('_format'),
        ];

        return $this->routeRedirectView('get_course', $routeOptions, Response::HTTP_CREATED);
    }

This is an example of a controller action from the new version of CodeReviewVideos.

This controller action allows me to create new courses, and do the majority of the common stuff inside the controller action.

I use a form to ensure the data being submitted is valid according to my business rules.

I create the course with a factory, because I no longer use Doctrine’s lifecycle events for created at / updated at.

I still save / persist the new course in the controller action. More on this in a sec.

I then dispatch an event to say, hey, a new course has been created.

At this point other interested parties (registered listeners / subscribers) can ‘hear about’ this event, and take some appropriate action.

Examples of this are writing some data to the log. Or in this case specifically, indexing the new course within Elasticsearch.

The good news is the controller action needn’t be concerned with any of that stuff. It just ensures an action is dispatched, and my concerns are nicely decoupled.

Back to why I persist in the controller still, though.

I did try moving this logic to a separate subscriber, and it did work. But I didn’t like it. It didn’t feel right, for me. Your opinion may vary, of course, and that’s cool too.

#3 – Keep Constant, and Dispatch Events from Services

There’s two parts to this video.

First, rather than rely on strings for our event names, it’s better – in my experience – to use a Constant instead.

To begin with we cover one approach to doing this. It may seem trivial, but on larger projects your sanity will be thankful 🙂

Secondly, and tying in with the video #1 above, we look at how we can dispatch events from our own Symfony services.

This second part is almost a recap of video #1, albeit a little faster, and more specific to dispatching events. The reason I cover this topic twice is because the original email I received regarding events mentioned this particular problem, which led to me creating this new short series on Symfony events in the first place.

That’s all from me for this week. Until next week, have a great weekend, and happy coding.

Chris