Symfony 4 Tutorial Content Update

Symfony 4 has caused me a few headaches this week. In between the usual site work, I’ve been tackling a noticeable up-tick in support requests regarding video content.

There are a number of reasons why this is, some of my own making.

Anyway, though I had planned to work on finishing each of the currently in-progress courses, I am instead switching to updating every existing Symfony related course on the site to use Symfony 4.

I’m honestly not sure how much work this will involve at this stage. I’m expecting it to take a few human days in total, which I unfortunately don’t have in one large block.

What this will mean is for each course I will pull the code from GitHub, and add a new branch, or branches, to enable support for Symfony 4.

I don’t expect, nor plan to have to update every video.

I do expect to have to record some new video content around updating some of the bigger projects.

I’m going to work on the courses based on the volume I’ve been receiving in support requests.

I’ll know and share more on this next week.

Video Update

This week saw three new videos added to the site.

#1 – Simple Backup Shell Script Linux

Backing stuff up is a real chore.

During development it’s not that big of a deal. Sure, if we accidentally destroy the DB it *might* be a pain, but it’s unlikely to cost us our jobs, or make national news (or, at least, hackernews).

Once we hit production however, backups are up there with the toppest of top priorities.

I guess we could do manual backups. It’s a good way to waste 30 minutes every day by following some regimented process.

A better way is to delegate to our robotic helper: the computer.

In this video we cover a basic but “good enough” (for me, at least) method of backing up pretty much anything, and keeping only the last X backups (where X may be 7 days for example).

We’ll cover how to write a backup script using Bash shell scripting, and how to automate running that script using a cronjob.

#2 – How to change the DB with a Phoenix Migration

Most every application I work on involves the database in some way.

When starting a fresh application it’s fairly easy to remember how and why the database schema ended up the way it is.

Then, after chopping and changing between four other projects, I come back to this “small” project and I’ve forgotten everything about it. Including why one of the fields is inexplicably an integer when a boolean seems like it might have been a better choice.

Fortunately, if you’re using database migrations, you can easily check back through the Git logs and figure out just what was changing in the code when you made such an interesting schema change.

I once read, somewhere, that having database migrations is effectively like adding version control to your database. I’m not entirely convinced by this, but they are helpful, and have proved worthwhile for me over the last few years.

Whatever your opinion, database migrations are available to us right “out of the box” with Phoenix, so using them is a no-brainer.

#3 – Database Schema: What and Why

In the previous video we covered how to tell the database about our changes, be it adding a new table, editing an existing table, adding or updating an index, and so on.

In order to work with that table we must write some Elixir code that represents that table.

A schemer

This code is called a Schema.

In this video we cover how we can generate a Schema for any particular table, and what the generated code is doing for us, and why.

Once we have a Schema, in this case for our “Contact”, we can start adding new records to the database. And we will do just this by making use of the REPL.

There are some interesting extras included in this video, such as touching on the purpose of alias, working with dates and times, and our first encounter with Pattern Matching.

That’s a Wrap

Ok, that’s it from me this week.

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

Chris

This Is What My Brother Found When Trying Symfony

My brother recently tried to get up and running with Symfony. His theory was that learning Symfony (out of all the available PHP frameworks) seemed like a good idea, being that he could ask me questions as and when he got stuck.

I anticipated that he would have problems with the “typical” problem areas:

  • Security
  • Forms
  • Database interaction

And maybe, given it is his first time using the MVC pattern, some of the concepts around that area, too.

What I didn’t anticipate was that he couldn’t even get started.

He experienced so many issues with installing Symfony, that after a couple of hours effort – with essentially nothing to show for it – he gave up.

I could sense his frustration.

Here’s a guy who spends all day working with PHP (mainly WordPress I believe), and after 8+ hours at his desk, he comes home, grabs a bite to eat, and tries to improve his dev chops by learning about Symfony.

For most of us, time is extremely limited, and spending two hours sat at home after doing 8 or more at your desk is a pretty big ask.

To come away with nothing (except a bad experience) is, understandably, enough to put him off.

What follows may appear heretical:

I suggested he try Laravel.

The next evening he did, and was up and running in less than ten minutes.

This got me to wondering just how many others are suffering from a similar fate?

I am well aware that Symfony has a steep learning curve. Laravel does too, if you are brand new to modern PHP practices.

What I hadn’t factored in was just how difficult it is to get the “hello, world” of Symfony up and running.

Coincidentally, around the same time I got an email from a site member (thank you Parminder) stating the following:

Please note that I am not a php developer and I haven’t dabbled at all with it beyond setting up the server to work with the react/redux/sagas project on your site. I think it would be useful to include instructions in the intro to the course on setting up the server for someone who is not familiar with Symfony/backend and just wants to focus on the React/Redux material – this would probably help with course uptake I think.

And in response to my reply, Parminder also shared the following:

It did take me a while to set up the Symfony server for the course on my mac and it no doubt is a barrier for people taking up your course, which is a shame because your material is very good.

Ok, so after I had told Mr Worf to stand down from Red Alert, I started to give this problem some serious thought.

Firstly, up until very recently I had not envisaged anyone who isn’t a PHP developer would join the site.

I’ve been proven wrong on this point.

Secondly, I have seemingly forgotten to cover a fundamental part of working with Symfony – getting set up.

I’m going to remedy this with a forthcoming video series. I will try and cover set up from the three major operating system variants – Windows, Mac, and Linux.

I will offer some suggestions as to how I’ve worked, and touch upon Vagrant, Ansible, and Docker.

Another really good suggestion that I’ve had from Parminder is to spin up a standalone server which none-PHP devs, or those who do not wish to spend the time doing all the setup themselves, can access my server instead. This would likely be on a 24 hour cronjob to reset the DB, or similar. This is something I will implement moving forwards.

It pains me that Symfony is potentially losing new devs at the very top of the funnel. There’s so much good stuff to be had from using the Symfony framework. I’m not just talking about developer productivity, but the wider benefits like:

  • a well paying job
  • an enjoyable and (generally) intuitive stack to work from
  • a community filled with incredibly smart and friendly folk

I’ve said before that learning Symfony has changed my life (and yes, for the better :)), and it’s part of my mission to share that same joy with you.

I’d love to hear your experiences of what it was like getting started with Symfony.

Was there a specific issue you hit upon?

Has this process improved as you’ve used Symfony more and more, or is the pain still there?

Please hit reply and let me know.

Video Update

This week saw three new videos added to the site.

We are coming to the end of the first phase of the Wallpaper Website.

https://codereviewvideos.com/course/let-s-build-a-wallpaper-website-in-symfony-3/video/doing-the-little-before-the-big

Firstly we start by cleaning up our project.

Swapping and changing between branches in order to see how to write code with and without tests starts off fairly easy, but as the underlying design diverges, the setup (fixtures etc) need altering in differing ways for both branches.

There’s other stuff to cover here, too, such as the naming of files (and associated tests). It sounds almost trivial, but I’m sure you’ve experienced occasions when writing code where the names of things has started to feel… not quite right 🙂

Housekeeping is an important, if underappreciated aspect of the lives of us as developers.

https://codereviewvideos.com/course/let-s-build-a-wallpaper-website-in-symfony-3/video/tested-image-preview-sort-of

In the un-tested approach we added in a nice little preview snippet which allowed us to see our existing uploaded wallpaper image when going to the Update screen.

We’re going to add the same functionality into our tested version, but we won’t be able to directly test the template. Is this a problem?

https://codereviewvideos.com/course/let-s-build-a-wallpaper-website-in-symfony-3/video/finishing-up-with-a-tested-wallpaper-update

Finally we wrap up the test-driven approach to Wallpaper Updates. We cover not just the “happy path”, but two potential sources of bugs in our form setup.

This is really the major benefit of writing tests, as far as I am concerned. They make you think about the bad times as much as the good. It sounds very pessimistic, but 80% of the headaches I have as a developer stem from having not given enough thoughtful consideration to what might happen if things don’t go according to plan.

All we have left to do now is add in a tested approach to Deleting wallpapers (and their associated uploaded image files), and adding a little security. After all, we don’t want just any Tom, Dick, or Harry accessing our Admin panel.

That just about wraps up this week’s update. Thank you very much for reading, and I hope you have a great weekend.

Until next week, happy coding,

Chris

Symfony 3.3 Dependency Injection Example

Symfony 3.3 landed this week.

My most used addition so far has been the new Dependency Injection features.

Even though I’m not a huge fan of autowiring, I do very much like the new autoconfigure.

This is a cool new feature that allows the removal of tags from your service definitions.

Note that I use my services like this:

# app/config/config.yml

imports:
    - { resource: parameters.yml }
    - { resource: security.yml }
    - { resource: services/console_command.yml }
    - { resource: services/command_handler.yml }
    - { resource: services/event_listener.yml }
    - { resource: services/event_subscriber.yml }
    - { resource: services/factory.yml }
    - { resource: services/queue.yml }
    - { resource: services/repository.yml }

# Other typical Symfony config.yml stuff continues here

The important thing here is that stuff is split up.

What this means is that I could adopt the new approach in phases.

I started with the event_subscriber service list, simply because that was the next thing I needed to work on.

If you don’t have a decent set of tests covering your services – and breaking said services would render you jobless – then please add tests before doing this.

Here’s my services/event_subscriber.yml configuration before the change:

services:

    a6.event.subscriber.registration_mailing:
        class: AppBundle\Event\Subscriber\RegistrationMailingSubscriber
        arguments:
            - "@twig"
            - "@mailer"
            - "%support_email_address%"
            - "%fos_user_from_email_name%"
        tags:
            - { name: kernel.event_subscriber }

    a6.event.subscriber.visitor_support_request:
        class: AppBundle\Event\Subscriber\VisitorSupportRequestSubscriber
        arguments:
            - "@logger"
            - "@a6.mailer"
            - "@doctrine.orm.default_entity_manager"
        tags:
            - { name: kernel.event_subscriber }

    a6.event.subscriber.member_support_request:
        class: AppBundle\Event\Subscriber\MemberSupportRequestSubscriber
        arguments:
            - "@logger"
            - "@a6.mailer"
            - "@doctrine.orm.default_entity_manager"
        tags:
            - { name: kernel.event_subscriber }

And after:

services:
    _defaults:
        ####
        # all will be tagged:
        #         tags:
        #              - { name: kernel.event_subscriber }
        ####
        autoconfigure: true


    a6.event.subscriber.registration_mailing:
        class: AppBundle\Event\Subscriber\RegistrationMailingSubscriber
        arguments:
            - "@twig"
            - "@mailer"
            - "%support_email_address%"
            - "%fos_user_from_email_name%"

    a6.event.subscriber.visitor_support_request:
        class: AppBundle\Event\Subscriber\VisitorSupportRequestSubscriber
        arguments:
            - "@logger"
            - "@a6.mailer"
            - "@doctrine.orm.default_entity_manager"

    a6.event.subscriber.member_support_request:
        class: AppBundle\Event\Subscriber\MemberSupportRequestSubscriber
        arguments:
            - "@logger"
            - "@a6.mailer"
            - "@doctrine.orm.default_entity_manager"

That big comment wasn’t added for the sake of this post either. That is for real.

I like Symfony *because* it is explicit.

Other people, I know, prefer what I consider a more magical approach. I like magic in real life. In code, I appreciate magic, but I need to know what’s going on underneath before I feel comfortable using it.

If all my services were in one giant services.yml file, I would not use this approach. That’s my personal opinion anyway.

By the way, probably the most concise guide I have found on this topic has been “How to refactor to new Dependency Injection features in Symfony 3.3” by Tomáš Votruba.

Question Time

I would really appreciate it if you could hit reply, or leave a comment on the blog post to this one:

In a video tutorial series, do you care about seeing the Setup phase?

E.g. if the course was about using Symfony with Redis, how important is seeing Redis being setup and configured to you?

This would greatly help my video topics on a forthcoming course.

Thank you!

Video Update

This week there were 3 new videos added to the site.

https://codereviewvideos.com/course/let-s-build-a-wallpaper-website-in-symfony-3/video/wallpaper-setup-command-part-3-doing-it-with-style

Now that we have created a Symfony console command to find and import our wallpapers we are going to use Symfony’s console style guide to make it look nice.

https://codereviewvideos.com/course/let-s-build-a-wallpaper-website-in-symfony-3/video/doctrine-fixtures-part-1-setup-and-category-entity-creation

Lets add Doctrine Fixtures Bundle to our project to provide a starting point for our Wallpaper and Category data. This is both useful, and easy to do.

https://codereviewvideos.com/course/let-s-build-a-wallpaper-website-in-symfony-3/video/doctrine-fixtures-part-2-relating-wallpapers-with-categories

With our Wallpaper and Category entities in place we can go ahead and create some starting data for each. Beware a few gotchas which we cover in this video.

As ever, thank you very much for being a part of CodeReviewVideos.com, and have a great weekend.

Chris

How I Fixed: \Swift_Message::newInstance() not found

I had a recent requirement to overwrite the Mailer  class provided as part of FOSUserBundle.

There’s a protected method in this class as follows:

    /**
     * @param string       $renderedTemplate
     * @param array|string $fromEmail
     * @param array|string $toEmail
     */
    protected function sendEmailMessage($renderedTemplate, $fromEmail, $toEmail)
    {
        // Render the email, use the first line as the subject, and the rest as the body
        $renderedLines = explode("\n", trim($renderedTemplate));
        $subject = array_shift($renderedLines);
        $body = implode("\n", $renderedLines);

        $message = \Swift_Message::newInstance()
            ->setSubject($subject)
            ->setFrom($fromEmail)
            ->setTo($toEmail)
            ->setBody($body);

        $this->mailer->send($message);
    }

Seems fairly straightforward.

Notice that PhpStorm sees nothing wrong with this method:

Being the lazy dev, I started by copy / pasting the entire contents of this class to form the basis of my own Mailer  implementation. Cue confusion:

I’ve used this \Swift_Message::newInstance()  code before, so I know it works. A quick check of the Symfony docs, and the SwiftMailer docs both seemed to confirm that what I was trying to do was correct:

(I checked the docs for Symfony 3.2, 3.3, and 3.4)

I thought it was just PhpStorm being a bit weird, but then I ran my code and saw things like this:

Attempted to call an undefined method named “newInstance” of class “Swift_Message”.

Diving through the code did indeed seem to show no references to newInstance .

Quite odd.

Anyway, sending an email – whilst important – wasn’t super urgent, so I commented out the code and added it to my GitLab issues list.

Whilst browsing Twitter later that evening I noticed:

I then remembered that being hasty to try new and shiny things was probably the cause of my problems.

Sure enough it turned out I’m using dev-master of SwiftMailer in my project. A quick glance of the changelog:

6.0.0 (2017-05-19)
------------------

 * added Swift_Transport::ping()
 * removed Swift_Mime_HeaderFactory, Swift_Mime_HeaderSet, Swift_Mime_Message, Swift_Mime_MimeEntity,
   and Swift_Mime_ParameterizedHeader interfaces
 * removed Swift_MailTransport and Swift_Transport_MailTransport
 * removed Swift_Encoding
 * removed the Swift_Transport_MailInvoker interface and Swift_Transport_SimpleMailInvoker class
 * removed the Swift_SignedMessage class
 * removed newInstance() methods everywhere
 * methods operating on Date header now use DateTimeImmutable object instead of Unix timestamp;
   Swift_Mime_Headers_DateHeader::getTimestamp()/setTimestamp() renamed to getDateTime()/setDateTime()
 * bumped minimum version to PHP 7.0

This looks like the culprit:

removed newInstance() methods everywhere

Fixing this is really simple:

$message = \Swift_Message::newInstance()
    ->setSubject('My important message subject')
    ->setFrom($this->supportEmail)
    ->setTo($user->getEmailCanonical())
    ->setBody($body, 'text/html')
;

becomes:

$message = (new \Swift_Message('My important subject here'))
    ->setFrom($this->mailingFromAddress, $this->mailingFromName)
    ->setTo($user->getEmailCanonical())
    ->setBody($body, 'text/html')
;

And as is often the case, when the provided objects / methods are used properly, things do work 🙂

Update – There is already an open PR to fix this in the docs: https://github.com/swiftmailer/swiftmailer/issues/925

How I Fixed: Missing Headers on Response in Symfony 3 API

The first time this caught me out, I didn’t feel so bad. The second time – i.e. just now – I knew I had already solved this problem (on a different project), and found my urge to kill rising.

I wanted to POST in some data, and if the resource is successfully created, then the response should contain a link – via a HTTP header – to the newly created resource.

Example PHP / Symfony 3 API controller action code snippet:

    public function postAction(Request $request)
    {
        $form = $this->createForm(MyResourceType::class, null, [
            'csrf_protection' => false,
        ]);

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

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

        $myResource = $form->getData();

        $em = $this->getDoctrine()->getManager();
        $em->persist($myResource);
        $em->flush();

        $routeOptions = [
            'id'      => $myResource->getId(),
            '_format' => $request->get('_format'),
        ];

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

And from the front end, something like this:

export async function createMyResource(important, info, here) {

  const baseRequestConfig = getBaseRequestConfig();

  const requestConfig = Object.assign({}, baseRequestConfig, {
    method: 'POST',
    body: JSON.stringify({
      important,
      info,
      here
    })
  });

  /* global API_BASE_URL */
  const url = API_BASE_URL + '/my-resource';

  const response = await asyncFetch(url, requestConfig);

  return {
    myResource: {
      id: response.headers.get('Location').replace(`${url}/`, '')
    }
  };
}

Now, the interesting line here – from my point of view, at least – is the final line.

Because this is a newly created resource, I won’t know the ID unless the API tells me. In the Symfony controller action code, the routeRedirectView  will take care of this for me, adding on a Location header pointing to the new resource / record.

I want to grab the Location  from the Headers returned on the Response and by removing the part of the string that contains the URL, I can end up with the new resource ID. It’s brittle, but it works.

Only, sometimes it doesn’t work.

Response
body:(...)
bodyUsed: false
headers: Headers
  __proto__: Headers
ok:true
status:201
statusText:"Created"
type:"cors"
url:"http://api.my-api.dev/app_dev.php/my-resource"
__proto__:Response

Excuse the formatting.

From JavaScript’s point of view, the Headers array is empty.

This leads to an enjoyable error: “Cannot read property ‘replace’ of null”.

Confusingly, however, from the Symfony profiler output from the very same request / response, I can see the header info is there:

Good times.

Ok, so the solution to this is really simple – when you know the answer.

Just expose the Location  header 🙂

# /app/config/config.yml

# Nelmio CORS
nelmio_cors:
    defaults:
        allow_origin:  ["%cors_allow_origin%"]
        allow_methods: ["POST", "PUT", "GET", "DELETE", "OPTIONS"]
        allow_headers: ["content-type", "authorization"]
        expose_headers: ["Location"] # this being the important line
        max_age:       3600
    paths:
        '^/': ~

After that, it all works as expected.