PHP/Laravel: the good, the bad and the meh: Why we rewrote our app in Go

(Modern) PHP: Does it really suck?

Like many, many developers out there over the age of 30, I basically started my programming journey with PHP (and perl). Circa ~’05, PHP was the go-to language for the web, and Adobe Dreamweaver supported it out of the box! Most HTML was hand-written, most connections were absent TLS, and lots of form data was still being stuffed raw-dog right into SQL queries.

This, I estimate, is what is responsible for 69.420% of the hate PHP gets today. (The rest comes from JS andys who have never seen or written a line of it, who likely just want to deflect some of the flak)

When I was hired at my current job, due to my situation of being currently incarcerated see my other post, I was happy to find any dev work, and in absolutely no position to be picky about the tech stack. I learned that we would be using Laravel, and PHP 8.2 for the backend, and although I had less than enthusiastic feelings about PHP itself, I had heard good things about Laravel and dove in head-first.

Background info:
We are a non-profit tech startup building an education-focused platform to aggregate data from various integrations (Learning management systems), in which incarcerated students can access their coursework and grades, and correctional facilities can track their progress across multiple providers. The original product was developed in the MO state prison system by a handful of residents, and was written in PHP 5.6, with no internet access, no stack overflow, and no real-world development experience. It was truly impressive to see the result of this, and I am surprised to this day how much they were able to accomplish with such limited resources. This was the reason PHP was chosen as the primary language for version 2 of the platform.

The Good:
Since I had been away from the tech world for so long, I missed out on most ‘frameworks’. Rails was just blowing up when I was programming last, so I never got much experience with the concept of an HTTP server being abstracted away from the developer. Back then a lot of the PHP was inline in the HTML, and you just thought about the whole process differently. I soon realized that nearly every single thing you might want to do, Laravel not only had thought of it, but seemed to have a command that generates the necessary code to do it. If I had realized this sooner, I would have had a much more smooth experience with the platform. Unfortunately I spent the first few months trying to solve a lot of the common problems by hand first, before realizing that Laravel had a highly opinionated way of doing things, and that it was best to just follow the conventions. I’m not against an opinionated approach, in fact it’s one of the things I like most about Go.

Overall, Laravel, for me, is much more accessible than a framework like Django, which completely abstracts away any concept of a server (if you aren’t a django developer, and you were to see a django codebase for the first time, I’d be willing to bet pretty much whatever that unless you studied it pretty thoroughly and did some google’ing, you couldn’t tell me it’s an http server). Laravel has the straight forward and easily identifiable concepts you would expect, the ORM is actually pretty good, assuming you can configure your LSP and linter to properly recognize the methods, and the built-in migration system is easy to use. All in all, as far as frameworks go, Laravel is surprisingly good, and for a blog or simple e-commerce website, I’d take it over NextJS or Django any day (I’m not a Ruby dev so I can’t speak on Rails).

Modern PHP is very different from the PHP of old. It has some surprisingly good language features like Traits and match statements, however their enums are in competition with typescript for being the worst of any language I have seen. Modern PHP manages to take a rather straight-forward object oriented approach, at a bit more than the level of python. My personal preferences lean much heavier into the functional realm, so I was pleased to find that PHP now also provides some basic functional features too, like their ‘iterator’ package.

The Bad:

First lets go with the obvious… C++ and JS share this issue as well: backwards compatibility has just kept so much suspect nonsense in the language, that you are stuck trying to figure out which of the 4 ways to do X thing you should be using. PHP’s problem in particular, is that they seem to have switched the naming convention each time they designed one of these bad features, so you will often find a lower_snake, Pascal, camelCase, … versions of a slightly different named function and be stuck with that mess for a while. I don’t know if they switched the global naming convention every 5 years, but damn is there some inconsistency there.

Another fairly obvious one: I am heavily on team “strongly and staticly typed languages are better”, I write Rust in my free time on all my hobby or contract projects. Now, after having worked with PHP and Laravel for about 8 months, I can say that I am even more convinced. You may be able to write plenty of larger, more complex applications in Laravel with no problems, however if one day you need to perform a non-trivial refactor, you will be in for a rough time.

Some of the most common issues I have had with Laravel are the fact that because Laravel takes care of so many common things for you, you will define things like Requests and Resources, that come with methods on them validate the JSON bodies or serialize the returned database rows into a JSON response. This is all well and good, but many applications will have several Request and Resource types for each controller, and if you have multiple rows/fields change on each table, you will be having to edit files like these across many parts of your application. Anything you may miss, static typing will easily catch at compile time.

“Dynamic types are fine, testing fixes this problem…” copium

PHP’s linter and LSP are simply sub-par compared to what you are going to get with Gopls or Rust-Analyzer, or even something like TSServer. This isn’t necessarily a PHP or even an issue with Laravel, but the introspection tools are just not able to handle the complex types that you are working with in Laravel. Simply setting up a good linter took hours because it would throw errors on lots of valid code, and I had to outright disable it for certain files. (skill issue, I know. but I want the linter to just work)

Yes, PHP 8+ has some type hinting, but there is no compile time to enforce them at, so much of the usefulness is lost. I have had handlers where there was (declare(strict_types = 1)), declared: that didn’t throw errors until that specific handler had been called.

Now, Go’s type-system is far from perfect, and it’s famous lack of type safe enums/unions makes me cry a little bit inside.

type Color int

const (
  Red Color = 2
  Blue Color = 3
  Green Color = 4
)

func applyColor(color Color) {
  result := doSomethingWithColor(color)
  //...
}

func main() {
  var num int = 2340
  applyColor(num)
  // nothing wrong with this!, no problems here!   💀
}

Although this is far from my only issue with Go, (I’m looking at you, single letter variable naming convention -_- ) (w http.ResponseWriter, r *http.Request), modules, and capital-letter for public access… and mayyybe some more issues I won’t get into right now 😅

But what we did gain, on top of performance, type safety, long-lived processes, simpler deployments and the ability to move some functionality into independent services, was a very clear intention in our back-end of what is really happening. Go’s stdlib net/http imho, is the perfect amount of abstraction for writing most servers. It would be impossible to write back-ends in Go, and not have a solid fundamental understanding of what is going on. You are not simply figuring out X framework’s particular method of solving Y end-user problem, you are implementing the actual solution to it, by understanding what needs to happen.

Random example:

Lets go with basic auth middleware. Many projects rely on some outside service (we actually do now rely on Ory because we needed comprehensive identity mgmt for the environment), but still employ the good old JWT.

In Go, a popular way of implementing auth would be to install the JWT module, and create a Claims struct with some user + session information, encrypt it, and set the cookie in the browser. Then you make some middleware (which is exactly what you would think it would be, a function that accepts a handler, and returns a handler), to check the cookie, validate the JWT, and create a context.Context key value pair with the claims you can pass around to subsequent handlers, to make the relevant user session information available where you will need it.

This is a good example of how you are just forced to solve the problem itself, as opposed to trying to figure out exactly how to use X frameworks particular abstraction layer, having to read their docs and make sure you are doing all the required things in the right order to make this work. In Go, you are thinking about how to solve the specific problem of needing to validate each request sent by the user, and it forces you to understand what is really happening behind the scenes. I find often times that the problem itself is far more trivial than trying to figure out how to use the API of whatever ‘solution’ is being sold to you as a convenience. This leaves newer developers clueless as to how these things are actually implemented, and only teaches them how to use X tool.

Yes I know that this argument could be applied all the way down the stack, eventually ending in the semi brainrot argument of “you have to use only C or assembly for everything or it’s a skill issue” that’s been popular in some programming circles lately with the seemingly unemployed. But this is the particular layer where I’ll pick my personal hill to die on.

This abstraction level is fine for the majority of the web applications, and unless you need extreme performance, Go will serve quite well as a versatile, fast and simple back-end or service. Go services can easily run in scratch docker containers with almost no dependencies. Deployments and development environments are very simple as everything integrates well in the docker/container ecosystem.

Production php/laravel deployments are significantly more work and preparation, and you can say that containerizing is cheating, but even container free I am able to deploy our Go application on an ec2 in 1/4 of the time it would take me to deploy our old codebase.

That is enough of my rant for today. These aren’t two languages that are commonly/should be compared as they have very little in common, so I am not trying to do too much here. Overall I am much happier getting to work with Go every day over php, and so far it has been a great experience.

Does PHP suck as much as people say? No, definitely not.

Do I particularly want to use it for anything more complex than a blog or simple application? No, probably not.

At least with php, nobody is trying to use it for anything outside of it’s intended domain. 🥲

mistake

Thanks for reading this, questions or comments feel free to email me at preston@pthorpe92.dev

2024

Back to Top ↑

2023

Just learn everything

8 minute read

Unsolicited advice for anyone seeking to learn computer science or software development in 2023.

Gratitude

1 minute read

How I got here is already far too long, so I must include a separate post for all the credits and gratitude I need to extend to those who made this possible.

Back to Top ↑