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.

Budget Pro development begins again

The budget overview screen in Budget. Shows the this month and the two following months and the expenses for each

There was an initial flurry of development on Budget Pro in October/November and then it stalled. The stall was primarily down to lack of time as I started a freelance project in the November and needed to obviously dedicate time to that. However, whenever a free evening popped up, I chose not to work on Budget Pro and instead work on Budget or the freelance project to try and finish it sooner.

No sinister reason behind this and we still fully expect to release Budget Pro within the first half of 2023. The issue was simply that there were still things to learn from Budget, our free budgeting App.

Budget Open Beta

During the open beta, we learnt far more than we expected. We had to improve the demo, we needed to rework the Budget overview and there were several small features which we deemed necessary and didn’t finish adding until the end of the year.

In late November, I decided to delay any further development on Budget Pro until the official release of Budget; that way we would get to see and resolve all our problems in one codebase and not worry about having to rework anything in Budget Pro.

We always planned on there being two offerings of our budgeting tool – Budget, the free version and Budget Pro, our subscription option. In building Budget first, it has enabled us to identify and iron out potential issues before developing the much more complex version.

Our two Apps, Budget and Budget Pro solve the same problem, but they tackle it slightly differently. Budget is aimed at everyone; Budget Pro is aimed at users who need to collaborate or have much more complex budgets requirements.

Budget Pro is due out within the first half of 2023 and as soon as we are able, we will open it up for beta, the plan being we don’t make any of the same mistakes we did with the launch of Budget.

It is always caching!

In October I added multiple account deletion options to the Costs to Expect API – you can delete the data for a specific App, reset the data for a specific App or delete your account entirely.

There could be a large amount of data to delete so I decided to handle the deletes behind the scenes with jobs. This worked well and during all my testing, there was never an issue and if an issue does occur, I’ve added a notification to catch it and let me know.

However, after release, I tested with a throwaway account, and it wasn’t working. As a user I could reset my state but when I signed in again all my data was still showing.

For a little while I was perplexed. The API wasn’t erroring, the data was being removed but Budget still showed the old state. The first hint happened when I tried to do something in Budget; I immediately got an error from the API, a budget item couldn’t be created because the resource didn’t exist.

Aha! I knew what the problem was. I said to myself, “Budget has data, the API doesn’t, there must be a cache issue somewhere.

Budget doesn’t retain a cache so it can only be the API. I rush (really, I did) to my IDE and low and behold, my jobs don’t clear the cache after deleting the requested data – oops!

How did this happen?

Well, that’s easy. During development, I turn off the cache on my local instance of the API as I don’t want the cache getting in the way of any new features I’m writing or debugging. I test before I make a new release, manually and automatically. Though clearly, I’m not always turning cache back on.

The Solution

The solution is easy – do a better job of testing but it isn’t quite as simple as that. I’m going to update my automatic tests to include testing with and without caching enabled. More importantly though, I’m going to expose the API cache status.

It isn’t unusual for the development or staging version of your Apps to highlight their status – a bar at the top, a different background, a visual notifier of some kind. I’m going to update Budget and all the other Costs to Expect Apps to include this notifier and show the state of the API, is caching enabled? Is it in debug mode? That way when I’m testing a data state feature, I’m more likely to spot that status and think about the extra steps.

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.

Supporting multiple measurement units

I’m working on a freelance project in which there are multiple measurement units. For example, customers can provide their required flow rate in litres per second, litres per minute, litres per hour, cubic meters per hour, gallons a minute or imperial gallons per minute.

Warning: This gets really complicated, fast!

You might have noticed we have imperial and metric measurement units above. Well, that means we need to support mm, meters, degrees Celsius and the inferior imperial versions some people still use. You know who you are.

To try and keep it simple, I have ingress objects which take all values and convert them to a ‘known’ unit of measurement for each type. I can then process all my calculations and not need to be concerned with converting anything whilst I’m calculating. When done, I have egress objects which convert everything back to the required unit and format.

I’m also storing everything in the original format, and I’ve tended to find over the years that this is better than converting and then storing. Customers don’t tend to like it when a value of three returns as 2.99997 next time they look at the request.

I’m only a little way into the project but so far, having a single interface to deal with is making much easier as I only need to care about the units for a value at the time it enters the ‘service’ and the moment it exits the ‘service’ and is about to be presented on screen.

Hopefully, all goes well, only time will tell.

Blog posts on freelance projects are deliberately a little vague as I can’t really divulge too much, I can talk about general stuff but obviously I’m never going to mention specifics.