My typical project setup for Laravel

Laptop with movie style code

This is one of those posts that is more of a reference for me rather than general information, if however, you use PHP, Docker, MySQL, Windows, WSL2 and PHPStorm this post about my typical project setup may be of interest to you.

Docker and WSL 2

I use Docker, specifically docker compose. Typically, I define two services in the docker-compose.yml file, [project_name].app and [project_name].mysql. If the project is part of a service I will define a network, this is to make it easier for containers to communicate. All the Costs to Expect Apps rely on the Costs to Expect API so they all share the same network.

Below is an example of a docker-compose.yml file for Budget, our free budgeting tool.

version: '3'
services:
    costs.budget.app:
        build:
            context: .
            dockerfile: .docker/app/Dockerfile
        image: costs.budget.app
        container_name: costs.budget.app
        ports:
            - "80:80"
        volumes:
            - .:/var/www/html
        env_file: .env
        environment:
            TZ: UTC
            DB_HOST: ${DB_HOST}
            DB_DATABASE: ${DB_DATABASE}
            DB_USERNAME: ${DB_USERNAME}
            DB_PASSWORD: ${DB_PASSWORD}
    costs.budget.mysql:
        build:
            context: .
            dockerfile: .docker/mysql/Dockerfile
        image: costs.budget.mysql
        container_name: costs.budget.mysql
        ports:
            - "3306:3306"
        env_file: .env
        environment:
            TZ: UTC
            MYSQL_DATABASE: ${DB_DATABASE}
            MYSQL_USER: ${DB_USERNAME}
            MYSQL_PASSWORD: ${DB_PASSWORD}
            MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
        volumes:
            - ./.docker/mysql/data:/var/lib/mysql
networks:
    default:
        name: costs.network
        external: true

I typically leave the ports at their default values; the exception is when I know I will need to run more than Docker container at a time. I always need to run the Costs to Expect API at the same time as another App so will typically map the ports on the API to 8080 and 3308.

The dockerfiles for each of the services exist in a .docker folder. I typically end up with .docker/app/Dockerfile and .docker/mysql/Dockerfile. The .docker/mysql folder will also contain a data folder, this is the volume for the MySQL data.

The MySQL Dockerfile doesn’t include any configuration, just FROM mysql:8, the example below shows a Dockerfile for a typically Laravel application.

I used to include Composer and PHPUnit but have moved away from the additional complexity as it didn’t really offer any benefits and just made configuration in PPHStorm more compleicated.

FROM php:8.1-apache

COPY . /var/www/html
COPY .docker/app/vhost.conf /etc/apache2/sites-available/000-default.conf

RUN apt-get update && apt-get install -y \
        libfreetype6-dev \
        libjpeg62-turbo-dev \
        libpng-dev \
    && docker-php-ext-configure gd --with-freetype --with-jpeg \
    && docker-php-ext-install -j$(nproc) gd

RUN apt-get update && apt-get install -y \
    zip libzip-dev \
    && docker-php-ext-configure zip \
    && docker-php-ext-install zip

RUN docker-php-ext-install pdo_mysql bcmath

RUN chown -R www-data:www-data /var/www/html \
    && a2enmod rewrite

WORKDIR /var/www/html

PHPStorm, Composer and PHPUnit

PHPStorm works relatively flawlessly with WSL2, well it did and will again without a tweak after the 16th of January.

I store all my project files in WSL2, specifically Ubuntu, the project root for PHPStorm will normally be something like \\wsl$\Ubuntu\home\[user]\Projects\[project_name]. I have a Projects folder inside my home directory and a folder for each project. The Projects folder includes any phar files I might need so as not to duplicate them across projects.

The CLI Interpreter is set to my distro of choice, so in my case Ubuntu.

Composer settings are as defined as below.

  • Path to composer.json //wsl$/Ubuntu/home/[user]/Projects/[project_name]/composer.json
  • Execution, composer.phar
  • Composer.phar location \wsl$\Ubuntu\home\[user]\Projects\composer.phar

PHPUnit settings are defined as below

  • Remote Interpreter
  • Use composer autoloader
  • Path to script /home/[user]/Projects/[project_name]/vendor/autoload.php
  • Test runner default configuration file /home/[user]/Projects/[project_name]/phpunit.xml.dist

This project setup works well for all my projects as if I ever need to use a different version of PHP or MySQL I can update the dockerfiles for each service and rebuild. I recently needed to upgrade an App form PHP5.6 to 8.0, this setup made it easy to upgrade to each version of PHP and MySQL along the way and test at each step.

If I need to use Tailwind, SASS or other tools, I try to use them via WSL2 before going the Windows route.

After reading this you may wonder why I use Windows for development, there are three main reasons, one, I’m comfortable with Windows, two, I play games, three, I build my own PCs. The setup above will work regardless of your operating system of choice.

Don’t stiff clients, upgrade your stuff

Image shows a click with the text "Time to upgrade"

A client recently asked me to setup a Silverstripe site on their server. They were in the process of getting the files and database extract to me and then I would be good to go. I hadn’t used Silverstripe since 2008 but figured, why not? Setting it up on a new server shouldn’t be too much of an issue…oh boy, was I wrong!

After a while, I received the files and a database dump. Everything seemed ok although I was surprised to see an included vendor folder. I checked the composer.json file to see what I was working with and well, I was a little concerned. PHP was set to 5.6 and all but two requirements were set to dev-master and silverstripe/recipe-cms was set as 4.2.*, released sometime in 2018.

Get it working

OK, so the developers had installed Silverstripe in 2018 and never felt the need to upgrade it, not even once. So I thought, I’ll set up a dev environment and get going.
I created a Docker container with PHP5.6, MySQL 5.7, and a few extensions. I copied everything in, including the vendor folder and managed to get a half working website running. It wasn’t quite as easy as described as I needed to work out what extensions I needed and fix a couple of minor bugs, but the site was working.

I thought cool, now I can start upgrading the site. It was at this point I recalled every dependency was set to dev-master. So the first step was to set the correct version for each dependency. Well, the version as of 2018 and get a composer install to work.
I checked the installed.json inside vendor/composer and found the commit hash for each dependency. I then found the related version on GitHub and updated the composer.json. This was tedious work; however, I figured I was working towards getting a working composer.json. When done, I discovered this wasn’t going to work; the provided composer.json did not create the included vendor directory and there was too much stuff missing.

I decided to go back to basics and create a brand-new composer.json including every package listed in vendor/composer/installed.json. Eventually I got to the point where I could run composer install with no vendor directory or composer.lock file and get a working vendor directory. Awesome!

Although this worked, I couldn’t yet start upgrading as there was too much listed in my composer.json. I started checking each package to see if it was a core requirement or a dependency of something else and pruned the composer.json file down to the bare minimum. Several hours later, I again had a working composer.json file.

Upgrade it

A day in and I had a working Silverstripe site, running on PHP5.6 and MySQL 5.7. I was also confident I could clear the “vendor” directory and recreate it. As joyous as I was, I was no further along. I couldn’t put this online as I needed it to be running PHP8.0 at a minimum and MySQL8. Additionally, I was having to use composer 1.10.26 – that also had to change.

I started the upgrade process; I went step by step starting at 4.2.0 all the way up to version 4.10.0. At each release, I would trash the vendor directory and composer.lock file and see if I ended up with a working site. It was slow as I kept needing to tweak things. Every time Silverstripe added support for an updated version of PHP, I would rebuild my Docker container and test again, slowly fixing bugs and correcting dependencies as I went. As soon I was running PHP8, I switched to MySQL 8 and manually updated the database schema.

Stable & Supportable

The site is now sitting on a staging server for the client. There are a few small issues here and there that I need to fix, and I need to continue upgrading to the latest version of Silverstripe, however, for now, this client is happy.

The moral of the story – upgrade your stuff. If you are selling a website to a client, at least have the decency to upgrade it. Handing them a hot mess four years later is disgusting and the “development house” in question should be ashamed of themselves.

I’m not going to name the “development house” as I don’t know if this was a one off. I expect not but it isn’t for me say as I never dealt with them directly, I just had to pick up the mess.

Invokable controllers

For several years I have been a fan of invokable classes, typically to act as action classes for forms and to create jobs for queues etc.

With regards to forms, this works well. The validation and creation code is all contained in one class keeping code out of the controller. If all you need is validation and a little logic before saving/updating, this model works well. The model breaks down when the complexity goes up.

On a recent project, I needed to validate the request, convert the data, generate lots of additional values related to fluid flow and validate again. Only after I have calculated the viability of the submitted data, do I save the request.

My action classes have simple names – createGame, saveUser, – you get the idea. From reading the name of the action, you know what it does and when you look inside, you have an idea about what you will see. This doesn’t work when there are five or six steps that must happen before the “quote” is created.

Invokable controllers to the rescue – I get all benefits of invokable action classes without the negative of a giant action class which is doing more than its name suggests.

I rarely use invokable controllers but when I do, they are a life saver.