GeistHaus
log in · sign up

https://james.brooks.page/rss/feed.xml

rss
93 posts
Polling state
Status active
Last polled May 19, 2026 02:03 UTC
Next poll May 20, 2026 01:39 UTC
Poll interval 86400s
ETag W/"fec77f32d36d42c532864eafe8d97d78"
Last-Modified Wed, 06 May 2026 08:11:50 GMT

Posts

Announcing Berroku
Sudoku, meet Minesweeper. A berry-themed logic puzzle game I built with Claude Code, now live on the App Store.
Show full content

I've been quietly working on something for the last few months, and today I'm finally shipping it: Berroku, a berry-themed logic puzzle for iPhone and iPad. Think Sudoku, but with a sprinkle of Minesweeper.

The Berroku home screen, showing today's daily puzzles and the current streak What is Berroku?

Sudoku gives you the grid. Minesweeper gives you the clues. Berroku takes both, swaps the numbers for berries, and turns the result into the kind of puzzle I find myself reaching for on the train or in the queue at a coffee shop.

The rules are simple:

  • Place exactly 3 berries in every row.
  • Place exactly 3 berries in every column.
  • Place exactly 3 berries in every block.
  • Numbered clues tell you how many of the 8 surrounding cells contain a berry.

The numbers are your insight into the grid, and chasing them down to a solution is genuinely satisfying.

A Berroku puzzle in its starting state, with numbered clues scattered across the grid

Three new puzzles drop every day for free, across Standard, Advanced, and Expert difficulties. There's a streak system, Game Center achievements and leaderboards, a home screen widget, and full offline play. If you really get the bug, there's a one-time Pro unlock that opens up more puzzles.

A Berroku puzzle mid-solve, with berries placed and the surrounding clues partially satisfied Built with Claude Code

Here's the part that I think is most interesting: I built almost all of Berroku with Claude Code, with just a sprinkling of handwritten changes where I wanted to nudge things in a particular direction.

I used the Opus 4.7 (1M Context) model throughout. The 1M context window earned its keep on a project like this — the iOS app, the marketing site, and the App Store metadata all sat comfortably in scope without me having to shuffle things around.

It wasn't a single prompt or a single sitting. The UI went through a handful of looks before I settled on something that felt right on a small iPhone screen and on iPad. The widget, achievements, and Game Center integration each needed their own focused passes. My role through all of it felt more like a director than an author.

Relearning App Store Connect

The one part of this project that absolutely was not vibes-and-AI was App Store Connect. I haven't shipped an iOS app in years, and walking back into it in 2026 was its own little adventure — provisioning profiles, capabilities, screenshots at the right device sizes, privacy declarations, the lot.

I got there in the end, but if you're dipping your toes back into iOS after a long break: budget more time for the paperwork than you think you need. The code is the easy bit now.

Launching on Product Hunt

Berroku is also live on Product Hunt today. If you'd like to try it, leave feedback, or just throw an upvote at it, I'd really appreciate it.

If you spot something weird, have an idea for a feature, or just want to brag about your streak, please let me know. And if you end up building something with Claude Code off the back of this, I'd genuinely love to hear about it.

Happy berry-placing.

https://james.brooks.page/blog/announcing-berroku
Getting into public speaking
Here are ten things I learnt about public speaking.
Show full content

Inspired by a post by Dylan Beattie, I wanted to share my own experience and advice.

At the end of 2022, I woke up with a singular decision: I was going to start public speaking. Up until then, I’d only given a couple of meetup talks and a slot at a Laracon US "Science Fair", but I had never been on a big stage. Within a few months of that decision, I went from speaking at meetups with 40 people to standing in front of crowds of over a thousand.

After giving a dozen talks around the world over the last few years, here are the ten things I’ve learned.

Preparation & Logistics
  1. Start small. Don’t underestimate the power of local meetups. Giving both technical and soft talks in smaller, safer environments was crucial for building my confidence. It proved to me that I had the ability to deliver before the stakes got high.

  2. Practice is a form of respect. Holding an audience’s attention for 30–60 minutes is a privilege, and you should give them your best. You can always tell when a speaker is surprised their submission was accepted and hasn’t rehearsed. Even if I’m giving a talk for the fifth time, I practice relentlessly.

  3. Everything is a story. When you’re on stage, you are telling a story, even if you are live-coding. Think about the arc: What is the start? What is the end? How do you get there? Avoid jumping around or trying to tell too many stories at once. Keep the thread clear.

  4. Big fonts. Bigger still. This is especially important when live-coding. Make the font as big as you possibly can without losing context, then go one step further. I’ve watched Nuno Maduro during a tech check increase the font to a size he was happy with, and then on the day, he increased it further still. Go big!

On The Stage
  1. Take off your lanyard. It sounds minor, but it matters. Lanyards spoil pictures and bounce around distractingly while you are exploring the stage. Ditch it before you walk on.

  2. Start with a joke. I don’t feel truly comfortable on stage until the audience cracks a smile, or better yet, a laugh. If it’s at my expense, even better. It breaks the tension, and at that moment, I feel like they are on my side.

  3. Own the stage. Even if there is a lectern, don’t hide behind it immediately. Start in the middle of the stage, introduce yourself, pace a little, enjoy the moment, and take it all in. You’ll find you become much more comfortable once you’ve physically explored the space. Also, during a tech check (where you test your screen connects and everything looks good) I like to stand on the stage and take it all in with no eyes on you.

  4. It will be different on the day. You won’t nail your script perfectly, and that’s absolutely fine. You will deviate, you will ad-lib, or you might reference a previous speaker who gave you food for thought. Embrace that. Being human makes the talk better.

  5. Be yourself, but also be a performer. Giving a talk is ultimately a stage performance. You have to put yourself out there and become a slightly exaggerated version of yourself, speaking louder and with more energy than usual. Enjoy the show.

The Most Important Lesson
  1. Nobody wants to see you fail. My friend Freek van der Herten shared this advice with me at Laracon India, and it is the single most comforting thing to remember. Nobody wants to see you go on stage and crash out. The audience is rooting for you to succeed.
Bonus Lesson
  1. Take water with you. The "cotton mouth" is real. Some speakers mark specific slides in their deck as reminders to take a sip. I’ve seen speakers have coughing fits with no water nearby, and it is painful to watch. Beyond hydration, the water bottle is a tactical tool; it forces you to slow down, take a breath, and find your place again. Side note: yes, it feels weird drinking while hundreds of eyes stare at you, but do it anyway.
https://james.brooks.page/blog/getting-into-public-speaking
Supercharging Rate Limiting with Cloudflare
Enhance Laravel’s rate limiting by using Cloudflare’s Web Application Firewall (WAF) to block excessive requests at the edge, reducing server load and improving API performance.
Show full content

Recently on Laravel Forge, we’ve been seeing an increase in the number of malicious, unauthenticated, or throttled requests hitting various API endpoints. Thanks to our internal testing of Laravel Nightwatch, we’ve been able to detect and handle these quickly. However, while we use Laravel’s rate limiting functionality, our web servers still process these requests before ultimately rejecting them. These "dead requests" contribute to unnecessary server load, which can lead to performance issues or even downtime.

To mitigate this, we’ve enhanced our approach by combining Laravel’s rate limiting with Cloudflare’s rate limiting via its Web Application Firewall (WAF). This allows us to reject requests at the edge—before they even reach our servers — reducing server load and keeping our API endpoints responsive.

In this article, I’ll show you how we’ve implemented this and how you can do the same.

What is Rate Limiting?

Rate limiting is a technique used to control the number of requests a client can make within a specific time period. Essentially, it limits the rate at which requests can be made. This helps prevent abuse, such as denial of service attacks, and ensures that your server remains responsive.

Typically, rate limiting works by returning x-ratelimit-* headers in the response, which include details about the current limit, remaining requests, and reset time. Laravel uses this method, and we can leverage it to instruct Cloudflare to enforce rate limiting at the edge.

Rate Limiting on the Edge

Cloudflare’s rate limiting is found under the Security / WAF section of the Cloudflare dashboard. It allows you to set up rules that block requests exceeding a certain threshold before they hit your server.

Setting Up Cloudflare Rate Limiting
  1. Create a New Rule:

    • Field: URI Path
    • Operator: Wildcard
    • Value: /api/*
  2. Match Unique Requests:

    • Enable "Use Custom Counting Expression"
    • Header Value: authorization
    • Field: Response Header
    • Name: x-ratelimit-remaining
    • Operator: equals
    • Value: 0

    Alternatively, you can use an expression:

    (any(http.response.headers["x-ratelimit-remaining"][*] eq "0"))
    
  3. Set the Rate Limit Trigger:

    • Configure Cloudflare to block requests after a specified threshold. For example, if your application allows 60 requests per minute, Cloudflare can block the 61st request instantly.
  4. Define the Response:

    • Action: Block
    • Response Type: Custom JSON
    • Response Code: 429
    • Response Body:
    {"message": "Too many attempts."}
    
  5. Set a Block Duration:

    • Any request that exceeds the threshold during this period will be automatically blocked.
Why Cloudflare?

Rate limiting ultimately relies on respect — there’s nothing technically stopping a client from making additional requests beyond the limit. However, by using Cloudflare’s WAF, we enforce rate limiting before our servers ever process the request, preventing unnecessary load and improving overall system stability.

By implementing rate limiting at the edge, we reduce “dead requests” and ensure our API remains available and performant. If you're experiencing high server load due to excessive API requests, pairing Laravel’s rate limiting with Cloudflare’s WAF can be a super quick and effective solution.

https://james.brooks.page/blog/supercharging-rate-limiting-with-cloudflare
2024 Recap
Looking back over 2024.
Show full content

With 2024 now at an end, I’ve taken a moment to reflect on what has been an incredible and transformative year both personally and professionally. Here are some of the highlights:

Laser Eye Surgery

This was truly a life-changing experience. I’m amazed at how much it’s improved my day-to-day life, especially while travelling.

If you’re in the UK and thinking about surgery, feel free to message me—I’m happy to share my experience!

Speaking

I had the privilege to continue speaking at conferences around the world, including; Laracon EU, Laracon India, and PHP UK. It was an incredible experience to share knowledge, connect with the community, and learn from so many talented people.

In June, I helped organise and co-host Laravel Live UK alongside my good friend Joe Dixon was such a cool experience and a great way to connect with the UK Laravel community.

Career Growth

This year, I was promoted to Engineering Team Lead at Laravel, responsible for Envoyer, Forge and Vapor, while working with a fantastic team of five developers.

Later this year we'll be announcing a series of exciting updates to Forge.

Cachet 3.x Progress

Development on Cachet 3.x continued strong this year. Here’s what’s new:

PHP Stoke Meetups

This year, I hosted 4 PHP Stoke meetups. It’s always inspiring to see developers coming together to learn and share ideas.

Fitness Journey

In 2022, I started running. Unfortunately, my "career" was cut short due to shin splints. I switched to spinning classes and then to strength training. I’ve been enjoying the gym, and feeling stronger and better than ever. I've also been working with a PT to really push myself and see what I can achieve.

Previous Recaps
https://james.brooks.page/blog/2024-recap
A Poem Of Grief
A poem I wrote shortly after the passing of my brother and the birth of my first child.
Show full content
Warning: This post contains themes of grief and loss.

While searching through my Notes app yesterday, I came across a poem that I’d written 6ish years ago. I was in two minds of posting this, but I think it’s important to share the rawness of grief.

It was written shortly after the passing of my brother and the birth of my first child. My world was a blur, and I was struggling to comprehend and process the very mixed, raw emotions I was feeling.

My therapist had suggested I try experimenting with different mediums as a way to process what I was experiencing. I don’t remember much about writing this, but I remember thinking it wasn’t great. Ultimately, I ended up creating Happy Dev as a way to express myself and process my own grief.

In a car, speeding to the hospital,
Did I hear that right? The news felt impossible.
Heart pounding, hands gripping the wheel tight,
Fighting the dark, chasing the light.

We walk through the doors, my head is spinning,
"Is he okay?" I wonder, my chest tightening.
Then three words hit like a hammer to my head:
"Your brother, he's dead."

The spinning stops. The world caves in,
A broken heart where hope had been.
He's not alright — his fight is done,
My brother just eighteen, is gone.

I see my dad, stooped by the bed,
Holding his son's hand, his grief unsaid.
From birth to death, eighteen years flash by,
I see my brother's face and cry.
He's not even in peace on the day he dies.

I organise everything, as much as I can,
Because how do parents bury their child with a plan?
I call the funeral director; I see the sums,
Even in death, we owe the man.

The days blur; my brother still waits.
A funeral looms, held hostage by dates.
We play his favourite band and I take mum’s hand,
To tell her the truth, something impossible to say —
you can no longer see your boy today.

All the while, I'm about to become a dad.
How can a father find joy with grief so mad?
Joy and grief wrestle, tearing me apart,
Two worlds colliding, one breaking heart.

My daughter is born, just one month from the loss,
A fragile new life, a line I must cross.
A life for a life — some cosmic mistake,
Not a trade I would make, not a trade I could take.

https://james.brooks.page/blog/a-poem-of-grief
tweet.new
I bought a .new domain to simplify tweeting—say hello to tweet.new, your shortcut to posting faster on X!
Show full content

Last night I did something crazy... I bought tweet.new. Yep, I know we're not supposed to call it "tweeting", it's now "posting", but let's be honest, we all do it.

What is .new?

.new is a domain extension exclusively for performing new actions online: any act that leads to creation can have a quick and memorable .new shortcut associated with it. Help your customers take action faster. Less time clicking means more time creating.

https://get.new

There are already some really good use cases of .new:

Back to Twitter

Anyway, back to Twitter... Did you know that you can create a link to post a tweet, ahem, post? Just use the following URL:

https://x.com/intent/post?text={encoded_text}

So, for example, if I wanted to people to click a button that composes a tweet saying "I just read https://james.brooks.page/blog/tweet-new. Thanks @jbrooksuk!" the link would be:

https://x.com/intent/post?text=I%20just%20read%20https%3A%2F%2Fjames.brooks.page%2Fblog%2Ftweet-new.%20Thanks%20%40jbrooksuk%21

I've never been able to remember that URL, so I've created a new one: tweet.new.

It's a simple redirect to the above URL, but it's so much easier to remember. All you need to do is type tweet.new into your browser, or you can append the ?text= query parameter to the URL to pre-fill the tweet.

https://tweet.new/?text=This%20is%20awesome%21

If you like this post, please share it!

Questions Did this really cost you $400?

Yes, yes it did 👀

Why did you do that?
  1. It's actually useful. It's much easier to remember tweet.new than the intent URL.
  2. I think it's funny.
  3. It's hosted on Cloudflare and I'm using their redirect rules, so there's no maintenance.
Can I use it?

Yes! I implore you to use it, let's make the $400 worth it 😂

Can I buy it from you?

Nope, sorry. I'm keeping this one. However, if you'd like to sponsor the cost, I'm definitely open to that. You can sponsor me on GitHub.

Comment on Hacker News.

https://james.brooks.page/blog/tweet-new
Secrets of the Laravel Team Talk Video
Watch my Documenting Laravel APIs talk from Laracon AU.
Show full content

At the start of 2024, I gave a talk titled "Secrets of the Laravel Team" in which I talk about how Laravel works as a company, how we manage projects and continue to ship the high quality you come to expect of us.

I‘ve given this talk at:

https://james.brooks.page/blog/secrets-of-the-laravel-team-talk
Laravel Artisan Cheatsheet API
In October 2023, I updated the Laravel Artisan Cheatsheet to Nuxt 3.x. Along with several other changes, I wanted to add a new feature to the site, an API.
Show full content

In October 2023, I updated the Laravel Artisan Cheatsheet to Nuxt 3.x. Along with several other changes, I wanted to add a new feature to the site, an API.

Versions

The /api/versions endpoint simply lists all versions of Laravel supported by the site:

[
  "10.x",
  "9.x",
  "8.x",
  "7.x",
  "6.x",
  "5.x"
]

When Laravel 11.x is released, it will of course be available here too.

Packages

The /api/packages endpoint is similar in that it lists all packages supported by the site:

[
    "laravel/breeze",
    "laravel/cashier",
    "laravel/cashier-paddle",
    "laravel/dusk",
    "laravel/envoy",
    "laravel/fortify",
    "laravel/horizon",
    "laravel/jetstream",
    "laravel/passport",
    "laravel/pennant",
    "laravel/nova",
    "laravel/octane",
    "laravel/pulse",
    "laravel/sail",
    "laravel/sanctum",
    "laravel/scout",
    "laravel/socialite",
    "laravel/telescope",
    "livewire/livewire",
    "inertiajs/inertia-laravel"
]

All packages are listed by their Composer package name, not their display name. When building the documentation for each Laravel version, we'll attempt to install the packages listed on this endpoint. Of course, not all packages are available due to different versioning constraints.

Commands

To find the commands available for each version of Laravel, you may use the /api/{version} endpoint. For example, /api/10.x will return all commands for Laravel 10.x.

[
  {
    "name":"about",
    "description":"Display basic information about your application",
    "synopsis":"about [--only [ONLY]] [--json]",
    "definition":{},
    "aliases":[],
    "arguments":[],
    "options":[
      {
        "name":"only",
        "description":"The section to display",
        "value_required":false,
        "value_optional":true
      },
      {
        "name":"json",
        "description":"Output the information as JSON",
        "value_required":false,
        "value_optional":false
      }
    ]
  }
  // More commands...
]

I'd love to see what you can build with this API. If you do build something, please let me know on X (Twitter)!

https://james.brooks.page/blog/laravel-artisan-cheatsheet-api
TIL: macOS’ Hidden Gem – The "caffeinate" Command!
Did you know macOS has a built-in alternative to Caffeine? It’s called caffeinate and it’s awesome.
Show full content

We’ve all heard of (or used) third-party applications like Caffeine to keep our Macs awake during long-running tasks. But did you know macOS has a built-in alternative? It’s called the caffeinate command, and it’s a nifty tool included right in your system.

What is the caffeinate Command?

The caffeinate command is a feature included in macOS that prevents the system from sleeping. You can find it in /usr/bin/caffeinate. It’s essentially a way to tell your Mac, "Hey, stay awake for a bit, I’ve got things to do!"

How Does It Work?

When you use caffeinate, it creates assertions that alter the system’s sleep behavior. By default, it prevents idle sleep, but you can customize its behaviour with various options:

  • -d: Prevents the display from sleeping.
  • -i: Prevents the system from idle sleeping.
  • -m: Keeps the disk awake.
  • -s: Stops the system from sleeping when connected to power.
  • -u: Declares user activity, turning the display on and preventing sleep (with a default 5-second timeout if not specified).
Practical Example

Let’s say you’re running a long build process — maybe you're still using Gulp 👀. You can use caffeinate -i npm run build, and your Mac won’t go to sleep until the process is complete. It’s simple yet incredibly effective.

Why Use caffeinate?

Using caffeinate means you no longer need to rely on third-party apps to keep your Mac awake. It’s a built-in, powerful tool that gives you control over your system’s sleep behavior, perfect for software developers or anyone running long tasks on their Mac.

So next time you need your Mac to stay awake for a bit, remember this hidden gem. And, remember to stay caffeinated ☕

https://james.brooks.page/blog/macos-caffeinate-command
Documenting Laravel APIs Talk Video
Watch my Documenting Laravel APIs talk from Laracon AU.
Show full content

This year I’ve been giving a talk at various conferences and meetups. The talk is about documenting Laravel APIs and how we can utilise a package called Scribe to do this for us almost automatically.

I gave this talk at Laracon AU in November and it was recorded. The video is now available on YouTube and I’ve embedded it below.

https://james.brooks.page/blog/documenting-laravel-apis-talk
Review: Three Peaks GBR Commuter 22L
A review of the Three Peaks GBR Commuter 22L backpack
Show full content
Three Peaks GBR Commuter 22L backpack

Online I’m not known for much more than being a software developer. In real life, I’m obsessed with bags, specifically backpacks. I’ve owned a lot of bags over the years, and I’ve got a lot of opinions on them. After all these years, I’m still looking for the perfect one.

Note: This is not a paid / sponsored review. All links are direct to the Three Peaks GBR website.

A couple of months ago I purchased the Three Peaks GBR Commuter 22L backpack. Now that its had some use, I thought I’d do something different and publish a review. While I don't have pictures of the back pack myself, I'm currently using the marketing images from Three Peaks' website.

Review

The Three Peaks GBR Commuter 22L backpack is an excellent choice for commuters and travelers alike. With its thoughtful design and useful features, it proves to be a reliable companion for daily journeys. I've been using this backpack for a while now, and here's my take on its various aspects.

Three Peaks GBR Commuter 22L backpack in black with black tag Pros
  • Anti-Theft Pocket: The hidden anti-theft pocket at the rear is a practical addition, ensuring that my valuable belongings remain safe and secure during my travels. It gives me peace of mind knowing that my essentials are out of sight and reach of potential thieves. I especially like using this to store my keys / passport when travelling. I also tried storing my AirPods in there but found that I could feel them in my back more than I expected.
  • Ample Space for Laptop: The well-padded rear compartment provides a snug fit for my 17" laptop, protecting it from damage. I only have a 13" MacBook Air, but it means there's plenty of space even with a case.
  • Built-in USB Port: The built-in USB port is incredibly convenient. Though a power bank isn't included, it allows me to easily charge my devices on the go, making sure I stay connected throughout the day. Pair this with the a power bank such as the Anker 22000 MHa and you're good to go on your travels.
  • Eco-Friendly Construction: The use of recycled plastic PET bottles for manufacturing is a great initiative towards sustainability. The backpack's 600D RPET material feels soft yet durable, and its water-resistant exterior is a valuable addition for outdoor adventures. Even in heavy rain, my belongings have remained dry and secure.
  • Stand-Up Feature: One aspect I truly appreciate (and not seen in other bags I've tested) is that the backpack can stand upright by itself. This feature comes in handy when waiting in queues or during other situations when I don't want to keep putting the bag on and off.
  • Stylish Design: The sleek and simple design of the backpack, especially in the black with black tag variant which caught my eye. It not only looks smart but has also drawn several compliments from friends and stranfers alike, which is always a nice touch.
  • Designed for Tide Accessories Bag Integration: Combining the backpack with the Tide Lunch Bag and Accessories Bag has been a game-changer for staying organized during my travels. The Accessories Bag provides a dedicated space for my electrical items, keeping everything tidy and easily accessible.
  • Convenient Suitcase Compatibility: The inclusion of a horizontal strap on the back of the backpack is a neat touch — even if its primary purpose is to hide the anti-theft pocket. It allows me to easily slip the bag over the handle of my suitcase, making it a breeze to navigate through airports or train stations.
  • Plenty of Pockets: The backpack features several internal pockets: laptop sleeve, tablet / notebook sleeve, two stitched pockets at the bottom and a zip pocket at the top for easy access. I often use the zipped pocket for my AirPods, field notes book, pen and an iPhone cable so I can plug into the USB port and charge my phone.
Three Peaks GBR Commuter 22L backpack open Cons:
  • Water Bottle Storage: The back pack features pocket-like sections on both sides. I'm not even sure if they are pockets? Enlarging these sections would allow me to carry a medium-sized water bottle comfortably, adding more practicality to the design without compromising the internal space which, being 22L is already limited.
  • Breathable Rear Padding: Despite being described as "breathable," I didn't find the rear padding exceptional in terms of breathability during extended use.
  • Slippery Internal Pockets: The two internal pockets face inwards from the outside of the bag, which is convenient. However, I noticed that my power bank often slips out of these pockets when I unzip the bag, which is a bit frustrating when it's attached to the internal USB port.
  • Chest Strap: I've worn the bag while walking several miles and cycling. One thing I'd love is an optional chest strap, to keep it snug while moving around.
  • Size: I'm not sure if I'd call this a con, but I do think a 25L / 30L version of this back pack (with the issues above addressed) would make for some interesting bags.
Conclusion

Overall, the Three Peaks GBR Commuter 22L backpack is a highly recommended option for commuters and travelers seeking a reliable, stylish, and eco-friendly companion. While it has some areas that could be improved, such as the side section size and rear padding, its numerous advantages make it a great choice. I find it perfect for my daily needs, and the integration with the Tide Lunch Bag and Accessories Bag adds a whole new level of convenience and organization. With a very attractive price point of £60 ($78~) and 77 5-star reviews, it's evident that many others share my positive sentiment about this backpack.

https://james.brooks.page/blog/three-peaks-commuter-22l-backpack-review
My Git Aliases
Here’s how I alias Git commands and increase my productivity.
Show full content

Did you know that you can add custom aliases to the git command? I recently switched to a new Macbook and realised that I was missing some commands that it turns out I was using daily. Manually restoring them showed me two things:

  1. I need to use sync my dotfiles repository more.
  2. Git aliases are a huge part of my daily development workflow.

Before you add any aliases, you’ll need a .gitconfig file in your home directory, (for example, ~/.gitconfig). This file will often already exist to configure the name and email address to make commits with.

My Aliases
[user]
  name = James Brooks
  email = james@alt-three.com

[core]
  editor = nano

[alias]
  ; List all branches.
  branches = branch --format='%(HEAD) %(color:yellow)%(refname:short)%(color:reset) - %(contents:subject) %(color:green)(%(committerdate:relative)) [%(authorname)]' --sort=-committerdate

  ; Delete a branch.
  del = branch -D

  ; Show the last commit.
  last = log -1 HEAD --stat

  ; Undo the last commit.
  undo = reset HEAD~1 --mixed

  ; Add and commit everything with "wip" as the commit message.
  wip = !git add --all && git commit -m 'wip'

As you can see, I don’t actually have a lot of aliases. I’ve tried to keep it simple and only have aliases that I use regularly. I also don’t add aliases to shorten existing commands (without passing options).

Adding Aliases

To add an alias, we need to append commands to the [alias] section of our config file. The format is aliasName = aliasCommand.

You may also add an alias using the git config command:

$ git config --global alias.undo 'reset HEAD~1 --mixed'

There are a few things to know about when creating Git aliases:

  1. Aliases can run more than just one command. Take a look at the wip alias; it calls git add --all and then git commit -m 'wip'. You can also call other Git subcommands, such as git log or git status.
  2. You may’ve noticed that the undo alias does not call the git subcommand, whilst wip does. For aliases that simply run another Git command but with additional parameters, this is absolutely fine. However, if you want to run a command that is not a Git subcommand (or you're running multiple commands), you should prefix it with !. This stops Git from trying to run the command as a subcommand.
  3. Aliases don't need to run a Git subcommand. You could run another application for example, visual = !gitk.
  4. You could use an alias to make a shortcut for another command, for example p = push so you can now run git p.

You can learn more about how aliases work at the Git Aliases documentation.

Are you using aliases? Let me know @jbrooksuk or @james@phpc.social.

https://james.brooks.page/blog/my-git-aliases
Extending Laravel’s "about" Command
Let’s take a look into Laravel’s "about" command and how we can extend it to add our own application / package information.
Show full content

Last year, I contributed a new about command to the Laravel framework. This command prints out information about your application:

$ php artisan about

Environment .........................................................  
Application Name .............................................. Forge  
Laravel Version .............................................. 9.51.0  
PHP Version ................................................... 8.2.1  
Composer Version .............................................. 2.5.1  
Environment ................................................... local  
Debug Mode .................................................. ENABLED  
URL ...................................................... forge.test  
Maintenance Mode ................................................ OFF  

Cache ...............................................................  
Config ................................................... NOT CACHED  
Events ................................................... NOT CACHED  
Routes ................................................... NOT CACHED  
Views ........................................................ CACHED  

Drivers .............................................................  
Broadcasting ................................................. pusher  
Cache .......................................................... file  
Database ...................................................... mysql  
Logs .......................................................... daily  
Mail ........................................................... smtp  
Queue ......................................................... redis  
Session .................................................... database  
Extra things you can do with this command

Using the --only option, you may specify a comma-separated list of sections to display:

$ php artisan about --only=cache,drivers

Cache ...............................................................  
Config ................................................... NOT CACHED  
Events ................................................... NOT CACHED  
Routes ................................................... NOT CACHED  
Views ........................................................ CACHED  

Drivers .............................................................  
Broadcasting ................................................. pusher  
Cache .......................................................... file  
Database ...................................................... mysql  
Logs .......................................................... daily  
Mail ........................................................... smtp  
Queue ......................................................... redis  
Session .................................................... database 

Of course, you can also get the information in JSON format using the --json option:

$ php artisan about --json

{"environment":{"application_name":"Forge","laravel_version":"9.51.0","php_version":"8.2.1","composer_version":"2.5.1","environment":"local","debug_mode":"ENABLED","url":"forge.test","maintenance_mode":"OFF"},"cache":{"config":"NOT CACHED","events":"NOT CACHED","routes":"NOT CACHED","views":"CACHED"},"drivers":{"broadcasting":"pusher","cache":"file","database":"mysql","logs":"daily","mail":"smtp","queue":"redis","session":"database"}}
Appending your own information

What you may not know is that you can also add your own application or package information into the command’s output. For package developers, this allows you to share additional information such as drivers, versions etc. and for application developers, this allows you to add application-specific information.

To share information with the about command, you need to update your service provider’s boot method:

use Illuminate\Foundation\Console\AboutCommand;
 
/**
 * Bootstrap any application services.
 *
 * @return void
 */
public function boot()
{
    AboutCommand::add('My Package', fn () => ['Version' => '1.0.0']);
}

Yes, that’s all it takes.

Firstly, we pass through the "section" name. This is how the command will separate the output and of course, we can add to the default sections by passing the same names.

$ php artisan about --only=my_package

My Package ..........................................................  
Version ....................................................... 1.0.0

Notice how we can also specify my_package in the --only option?

Next, we're passing a callable that returns an array of key/value pairs for information add. Alternatively, we can pass an array of data, However, when passing a callable, Laravel will only evaluate the information when it executes the about command. This is a performance optimisation to prevent applications from always loading the information, even when it’s not needed.

Some packages are already extending the about command:

https://james.brooks.page/blog/extending-laravel-about-command
2022 Recap
With 2022 already being all but a distant memory, I thought it’d be nice to look back at the last year and reflect on the year long journey.
Show full content

With 2022 already being all but a distant memory, I thought it’d be nice to look back at the last year and reflect on the year long journey.

Laravel

Laravel Forge saw a lot of updates in 2022. Here are just some of the highlights:

We also shipped a lot of minor features and papercuts along the way, which really excites me. We sweat the details 🥵

I’m really proud of the work we do on Forge and the amount of time it saves developers every single day. As a customer, I couldn’t live without it! Oh, and there is a lot more to come here 😉

I also contributed a couple of features to the Laravel framework too:

Becoming a Laracasts Educator

In June, Laracasts released an updated series on Laravel Forge, created by me!

Laracasts Hero

Being asked to create a complete series (there are 24 videos) for Laracasts was an honour for me. Without Laracasts, I genuinely don’t believe I’d be working at Laravel now.

Also, being able to create this series was eye-opening. It’s extremely rewarding, but it’s also a lot of work. I already admired Jeffrey, but I have a new-found respect for the amount of effort he (and other educators) put into creating the material we take for granted.

Side Projects

I’ve continued working on Checkmango, the full-stack A/B testing platform I’ve been working on. Progress has been slow as I’ve been focused on lots of things in 2022, but it’s moving forward and that is all that matters.

The Laravel Artisan Cheatsheet saw a few updates, mostly to improve SEO. This year, I’d love to give it a visual refresh and upgrade to Nuxt.js v3.

I also released the Polestar Finder, a small web-app that notifies you when your Polestar 2 configuration becomes available. It was pretty popular on the Reddit and Facebook groups, and notified over 100 people that their configuration became available.

Speaking

I didn’t give any talks in 2022, but I did finally pluck up enough courage to submit a proposal to Laracon India, which was accepted!

I also started a new meet-up, PHP Stoke which will have its first meeting on the 14th January. PHP Stoke will be the first meet-up I’ve organised and my first talk of 2023.

Although I won’t be speaking at all of them, I’ll be attending:

I’d love to attend more meet-ups this year. If you’re organising a meet-up, please let me know and I’ll do my best to attend.

Education

I completed the St John’s Ambulance Mental Health Workplace Responder course in August. A good friend of mine asked whether I’d like to do the course and hold the position of "Mental Health Responder" for his business, which I jumped at the chance to do.

Aside from providing me with the skills for my friend, I also very much intend on restarting the Happy Dev podcast. I know, I’ve said this a few times now, but it really is true!

Running

2022 was the year that I really got into running. Strava says I ran a total of 165.7km over 38 activities and I spent 15 hours running... This is well over 100km more than I’ve run for any previous year.

I started attending our local park run (a 5k run) and that really got me going. I ran 14 park runs in 2022. Unfortunately, I did injure myself by pushing too hard, too quickly around August time and that led to shin splints which really set me back — I’ve learnt from that now.

For anyone interested, my current PBs:

  • 5k: 25:07
  • 10k: 57:13

I’ve also started 2023 by signing up for my first half-marathon (Potters ’Arf). The Potters ’Arf marathon is notorious for its hills and difficulty, so the training begins now!

Conclusion

These are just some of my 2022 highlights. I have a strong feeling that 2023 will be an even bigger year, packed full of excitement. I’m travelling a lot more this year and I can’t wait to see your beautiful faces.

Your Recaps

If you’ve written a 2022 recap, let me know and I’ll add it here:

https://james.brooks.page/blog/2022-recap
Upgrading macOS with Homebrew
Updates that break Homebrew happen so frequently and I forget how to solve it, that I’ve finally caved and documented it.
Show full content

After having my M1 Macbook Air for 2 years, I’ve upgraded to a refurbed M2. The form factor of the 2022 model is really nice and I love the slightly larger screen space. As much as I loved the old one, I was finding that having only two USB-C ports a pain. Now I charge and have two devices plugged in. I didn’t think it’d be a problem until I had started using it.

When it arrived, the first thing I did was install all the developer tools that I needed and got to work. But then I realised it didn’t come with Ventura, and now it’s upgraded Homebrew has fallen apart again.

git clone git@github.com:jbrooksuk/artisan.page.git
xcrun: error: invalid active developer path (/Library/Developer/CommandLineTools), missing xcrun at: /Library/Developer/CommandLineTools/usr/bin/xcrun

To fix this:

xcode-select --install

But sometimes, this doesn’t work and then instead, you need to run:

sudo xcode-select --reset

Which should fix it.

https://james.brooks.page/blog/upgrading-macos-with-homebrew
PHP Stoke
Introducing PHP Stoke, a meetup for PHP developers in Stoke-on-Trent.
Show full content

It’s with great excitement that I can say I’m hosting my first meetup; PHP Stoke.

PHP Stoke Hero

PHP Stoke is a joint venture with my friends at Aware Digital. Aware are a local Magento e-commerce agency based in Stoke.

I’ve wanted to host a local PHP meetup for a while, and it’s through knowing Aware that we’ve been able to make this work. We’ve been thinking about how we can make this a great event and I’m confident that we’ll achieve this on the day!

If you’re in Stoke-on-Trent and free on January 12th then please register and come say hi!

https://james.brooks.page/blog/php-stoke
Deployment Hook Error Handling in Envoyer
Laravel’s Envoyer service allows you to break up your deployment process into multiple steps, which makes it really easy to manage deployments. Envoyer runs each step individually, checking the exit code of the last command within the step. Because each deployment step is Bash, if it’s a non-zero exit code then it gets reported back as a failure.
Show full content

Laravel’s Envoyer service allows you to break up your deployment process into multiple steps, which makes it really easy to manage deployments. Envoyer runs each step individually, checking the exit code of the last command within the step. Because each deployment step is Bash, if it’s a non-zero exit code then it gets reported back as a failure.

A question I’ve been asked a few times is "why does Envoyer report this deployment step successful, when it actually failed?". Let’s look at an example of when this may happen, and then two ways we can resolve it.

The Deployment Step Example
php artisan migrate

echo "Hey, beautiful reader!"

In this example, let’s say that php artisan migrate may fail for some reason.

The Simple Solution

The simplest solution is to split the deployment step into multiple steps:

Run Migrations

php artisan migrate

Compliment The Reader

echo "Hey, beautiful reader!"

But this isn’t always the best solution, perhaps because we need to know the output of the response and do something else with it before reporting a failure.

The Correct Solution

The right way of handling this particular issue is to use more of Bash’s power, using the set builtin. We can update our deployment script:

set -e

php artisan migrate

echo "Hey, beautiful reader!"

set -e tells the script to exit immediately if a command (or pipeline of commands) returns a non-zero exit code.

https://james.brooks.page/blog/deployment-hook-error-handling-in-envoyer
A GitHub Action for Laravel Forge
Today I’m pleased to announce the availability of a new GitHub Action for Laravel Forge deployments.
Show full content

Today I’m pleased to announce the availability of a new GitHub Action for Laravel Forge deployments.

I want to thank @Glennmen for the equivalent Ploi action, as it’s heavily based on it.

Here is an example GitHub Workflow for deploying your application:

name: 'Deploy on push'

on:
  push:
    branches:
      - master

jobs:
  forge-deploy:
    name: 'Laravel Forge Deploy'
    runs-on: ubuntu-latest

    steps:
      # Checkout the repository to the GitHub Actions runner
      - name: Checkout
        uses: actions/checkout@v2

      # Trigger Laravel Forge Deploy
      - name: Deploy
        uses: jbrooksuk/laravel-forge-action@v1.0.1
        with:
          trigger_url: ${{ secrets.TRIGGER_URL }}

Please let me know if you’re using it!

https://james.brooks.page/blog/a-github-action-for-laravel-forge
Injecting Additional Data into Laravel Queued Jobs
We've shipped an enhancement to Laravel Forge that stores the ID of the user who initiated an event.
Show full content

Earlier today we shipped an enhancement to Laravel Forge that stores the ID of the user who initiated an event.

At first glance, you may be wondering why this is a big deal and you're probably asking yourself why I'm writing a blog post about it. All good thoughts. Let's dive into it!

In Forge, almost everything you can do to a server is handled within a queued job. Additionally, some of these jobs will dispatch other jobs via the sync connection. Forge has a ServerRepository class, most methods in the class are responsible for dispatching the relevant job, for example:

public function addJob(Server $server, $command, $user, $frequency, $minute, $hour, $day, $month, $weekday)
{
    $cron = $this->formatCronExpression(
        // ...
    );

    $job = $server->jobs()->create([
        // ...
    ]);

    Queue::push('Forge\Jobs\AddJob', [
        'server_id' => $server->id,
        'job_id' => $job->id,
    ]);

    return $job;
}

The most obvious way to achieve this is to go through every single method and add a User $initiator parameter, then pass that value in every place it's called. In the case of Forge, that would be very tedious, time consuming and not a great solution.

If, like me, you've read Mohamed Said's Laravel Queues in Action book, then you already know how powerful the Queue system is.

The Queue class contains a method named createPayloadUsing which allows you to register a callback that is executed when creating job payloads. This is exactly what we're after!

Queue::createPayloadUsing(function ($connection, $queue, $payload) {
    // $payload contains all of the job information, including the supplied data.
    $jobData = $payload['data'];

    if (! isset($jobData['initiated_by'])) {
        $jobData = array_merge($payload['data'], array_filter([
            'initiated_by' => request()->user()->id ?? null,
        ]));
    }

    return ['data' => $jobData];
});

This code is placed in the boot method of the AppServiceProvider.

We check whether the job's payload contains a key called initiated_by, if not then we add it to the array with the value being the current request's user's id.

Every single job initiated by Forge automatically knows who initiated it, and we can use this value when we're recording the event in the database.

https://james.brooks.page/blog/injecting-additional-data-into-laravel-queued-jobs
Installing Private Repositories using GitHub Actions
I've been testing out the new Laravel Spark package before it gets launched and implemented it into my new side project (more details coming soon).
Show full content

I've been testing out the new Laravel Spark package before it gets launched and implemented it into my new side project (more details coming soon).

Because I'm part of the Laravel GitHub organisation, I'm able to install the repository into my composer.json file like so:

{
    "repositories": [
        {
            "type": "vcs",
            "url": "git@github.com:laravel/spark-paddle.git"
        }
    ],
    "require": {
        "laravel/spark-paddle": "@dev"
    }
}

This all works great and I've been able to use Spark Paddle just fine however, when I pushed my changes to GitHub the tests immediately started failing because of the private repository.

My first thought was to use the ${{ secrets.GITHUB_TOKEN }} secret but this didn't work. After a lot of trial and error, this is the solution I came up with:

First, we need to generate a new token. This token needs full control of private repositories. You'll need to copy this token as it won't be displayed again. In our repository's secrets section, we need to create a new Repository secret. I called mine COMPOSER_AUTH and copy the token we generated into the value.

In our tests file, we need to tell Composer to use the new token:

- name: Install Dependencies
  env:
    COMPOSER_AUTH: '{"github-oauth": {"github.com": "${{secrets.COMPOSER_AUTH}}"} }' # [tl! **]
  run: |
    composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction --no-suggest

That's it. Our tests will now use the token we generated and we'll be able to access all of the repositories that we have access to.

https://james.brooks.page/blog/installing-private-repositories-using-github-actions
Laravel Form Request Tip
Over Christmas I started tinkering with a little project to learn about some of the emerging technologies and frameworks that I don't have a chance to play with day to day. For this project, I've been using Jetstream 2 and Inertia.js, which I've loved! I'd like to write a bit more about these when I get chance.
Show full content

Over Christmas I started tinkering with a little project to learn about some of the emerging technologies and frameworks that I don't have a chance to play with day to day. For this project, I've been using Jetstream 2 and Inertia.js, which I've loved! I'd like to write a bit more about these when I get chance.

Whilst I've been working on this, I've been trying to use some of Laravel's features that I've not really used much before. One of these is the FormRequest feature. A quick php artisan make:request CreateCampaignRequest command and all I need to do is authorize the request and add my rules — how great is that?!

Part of my choice in using Jetstream (aside from, well, everything about it) is that I was able to support teams out of the box. That's great, but now my app needs to validate differently based on the team. For example, two teams can use the same "Campaign" name, but one team can't have two campaigns with the same name.

As an aside, I always create a app/helpers.php file in my project and have started adding this to my Jetstream projects:

if (! function_exists('current_team')) {
    /**
     * Get the user's current team.
     *
     * @return \App\Models\Team|null
     */
    function current_team(): ?Team
    {
        return optional(auth()->user())->currentTeam;
    }
}

In my requests I need to validate that the campaign name is unique for the current team. That's easy enough:

use Illuminate\Validation\Rule;

public function rules()
{
    $currentTeam = $this->route('team') ?? current_team();

    return [
        'name' => [
            'required',
            Rule::unique('experiments')->where(function ($query) {
                return $query->where('team_id', '=', optional($currentTeam)->id);
            }),
        ],
    ];
}

It's not awful, but it's not great either. And what about if I need to access other route bindings or values?

To solve this, I created a new BaseRequest class, which overrides the prepareForValidation method to merge in data that won't exist in the original request itself. Because I'm also using route model bindings, I can easily pull each model back from the request:

class BaseRequest extends FormRequest
{
    /**
     * Prepare the data for validation.
     *
     * @return void
     */
    protected function prepareForValidation()
    {
        $currentTeam = $this->route('team') ?? current_team();

        $this->merge([
            'campaign_id' => optional($this->route('campaign'))->id,
            'subscriber_id' => optional($this->route('subscriber'))->id,
            'sub_campaign_id' => optional($this->route('sub_campaign'))->id,
            'team_id' => optional($currentTeam)->id,
        ]);
    }
}

Now I can quickly access any of this data within the rules. Our form request now becomes:

use Illuminate\Validation\Rule;

public function rules()
{
    return [
        'name' => [
            'required',
            Rule::unique('experiments')->where(function ($query) {
                return $query->where('team_id', '=', $this->team_id);
            }),
        ],
    ];
}

Of course, the prepareForValidation method can be used for a lot more, but this is just one use-case where I found it particulary nice.

https://james.brooks.page/blog/laravel-form-request-tip
Building The Laravel Artisan Cheatsheet
Introducing the Laravel Artisan Cheatsheet, a bookmarkable and shareable resource for all Laravel's default artisan commands. The source code for this resource is available on GitHub
Show full content

Today I released the Laravel Artisan Cheatsheet, a bookmarkable and shareable resource for all Laravel's default artisan commands. The source code for this resource is available on GitHub.

In the past I've tweeted several tips for artisan and the many unknown features and secrets it holds. I've had the idea to create this resource for a while and last night I finally made the time turn it into something I can share.

After make some last-minute adjustments, I finally deployed it. This evening, I've switched the project to use Nuxt.js. This brings a couple of benefits:

  • Generated output - Google loves this!
  • Work on additional features such as; keyboard shortcuts, multiple versions and synopsis tooltips.
  • Easier for other people to contribute.
  • The core
  • The core of the resource is a JSON file that holds all of the data needed to generate the command output. To generate this file I used Tinkerwell and this snippet:
$commands = Artisan::all();

$output = collect($commands)->sortBy(function ($command) {
  return $command->getName();
})->map(function ($command) {
  return [
    'name' => $command->getName(),
    'description' => $command->getDescription(),
    'synopsis' => $command->getSynopsis(),
    'definition' => $command->getDefinition(),
    'aliases' => $command->getAliases(),
    'arguments' => collect($command->getDefinition()->getArguments())->map(function ($argument) {
      return [
        'name' => $argument->getName(),
        'description' => $argument->getDescription(),
        'default' => $argument->getDefault(),
        'required' => $argument->isRequired(),
      ];
    })->values()->all(),
    'options' => collect($command->getDefinition()->getOptions())->map(function ($option) {
      return [
        'name' => $option->getName(),
        'description' => $option->getDescription(),
        'value_required' => $option->isValueRequired(),
        'value_optional' => $option->isValueOptional(),
      ];
    })->values()->all(),
  ];
})->values()->toJson();

Not all of this data is currently used, but the resulting JSON file should contain enough information to future proof the resource.

Anyway, let me know what you think and if you have some ideas, I'd love to see a PR!

https://james.brooks.page/blog/building-the-laravel-artisan-cheatsheet
AWS CLI - S3 and Alibaba Object Storage Service
How to configure Laravel Forge’s database backups with Alibaba OSS.
Show full content

A Laravel Forge customer recently reached out to us asking whether the Database Backup feature supported Alibiba OSS (Object Storage Service).

Since OSS is S3 compatible, the answer is happily "yes" however, in this case the S3 compatibility requires a custom configuration change which we can make very quickly. Without the below change, you'll get this error:

upload failed: - to s3://bucket/directory/file An error occured (SecondLevelDomainForbidden) when calling the PutObject operation: Please use virtual hosted style to access.

When Forge configures database backups, it creates a /root/.aws/config file that is used by awscli to configure how S3 settings. For OSS to work, I had to make the following one-line adjustment:

[profile backup-xxxx]
output=text
region=
s3 =
    signature_version = s3v4
+   addressing_style = virtual

The backup can now finish successfully.

https://james.brooks.page/blog/aws-cli-alibaba-object-storage-service-oss
2020 Recap
I recap on what happened over the year of 2020.
Show full content

Well, 2020 has been a rollercoaster of a year hasn't it 😬

Luckily my family, friends and I have mostly avoided catching the virus so far. I've been fortunate that I've been able to continue my job at Laravel with (almost) zero disruptions to my daily work life.

I'd like to take a look back at 2020, both professionally and personally.

At Work

I feel like this year I've really settled into my role at Laravel. I've crafted, deployed and supported several large features both in Forge and Envoyer.

Sharing these developments with the community via Twitter and the Laravel Blog has been important to me this year. I've made an effort to document and share more.

I previously reviewed my first year Laravel, but since then we have done so much more.

Laravel Internals Podcast

On the 18th November, my colleague Nuno Maduro and I recorded the first "Laravel Internals" live podcast on YouTube.

Laravel Internals is a new format where members of the team will discuss what they've been working on.

Forge

Most of my time this year has been spent working on Forge, which I've absolutely loved. Here are just some of the things I've worked on this year:

I've also tweeted about a lot of the smaller changes, features and enhancements that we've deployed. Some of these changes include:

Envoyer

Envoyer has received a lot of love this year too:

At Home

A lot has happened in my personal life.

A New House

After a turbulent few months at the beginning of this year, we were unsure if we'd be able to move. It look a lot of persistency and taking matters into our own hands, but we were able to move house in July.

This is the first home I've ever bought and I'm really proud of Katie and I for making it happen. We have a lot of jobs to get through, but thankfully lockdown has given me a lot of time to make a start on them and it's been fun!

A New (Home) Office

Having worked from home full-time since July 2019, I knew that wherever we moved to, I'd want a permanent "home-office" setup. We'd looked at places with enough outdoor space that I could have a separate working space, but in the end we settled for me using a spare bedroom and having a sofa-bed in there.

This was the first room to be fully decorated and, once life returns to more of what it was, we'll order the sofa bed to go in there.

Katie switched job this year but has also started working from home during the pandemic, so we're now sharing a desk and office.

I really need to take some new pictures of the office! We have a new carpet and some pictures up.

Celebrating Our First Wedding Anniversary

We celebrated our first wedding anniversary in July! I continue to feel very lucky that we chose to marry last year (we had considered waiting till this year) and I know of many weddings that have been affected by the pandemic.

Unfortunately, we were unable to do anything big, but we had a lot going on already (what with moving house), so we enjoyed our new home together.

A New Van

Katie and I have wanted to buy a campervan for years and the opportunity came this year.

We bought Birtha (a VW T25) in August with our friends. Sadly, we haven't had much chance to use her yet and when we did want to use her, we found she'd been leaking petrol!

Since replacing the petrol tank, our friends took her on a roadtrip to Scotland and back.

Next year, we need to spend a bit of time working on her and fixing up a few things.

Kelly and I sitting on Birtha!

A New Baby

We're expecting our second baby in June 2021! Katie is doing well and is having less morning sickness than she did with Harriet.

Music

Music is a big part of my life. I tend to obsessively stick with a few songs for a while and then spend a month or so rotating them as part of my previously obsessed songs.

This year has been no different. Here are some of the songs I've been listening to on repeat (not all new music):

  • AJR, Bang!
  • Frozen, Into The Unknown (yay for having a 2 year old daughter)
  • Freddie Mercury, Living On My Own - No More Brothers
  • Jake Bugg, All I need
  • Twenty One Pilots, Level Of Concern
  • You me At Six - SUCKAPUNCH
  • Hollywood Undead - Ghost Out
  • Madeon - The Prince
  • Bastille - WHAT YOU GONNA DO???

And I think that wraps up 2020 for me! Please do share your recaps with me, @jbrooksuk.

https://james.brooks.page/blog/2020-recap
Laravel Localization Case Tips
Here is a little tip for Laravel’s Localization that I discovered today whilst looking through the open Laravel Nova issues. Although it’s documented it’s not well known - at least, I didn’t know about it before today!
Show full content

Here is a little tip for Laravel's Localization that I discovered today whilst looking through the open Laravel Nova issues. Although it's documented it's not well known - at least, I didn't know about it before today!

Say you have an en.json translation file like this:

{
    "The :resource was created!": "The :resource was created!"
}

You can now use this like so:

__("The :resource was created!", ['resource' => 'category']) // "The category was created!"

But let's say you need to translate this to a language where the resource name must be in capitals. In our locale file, we say:

{
    "The :resource was created!": "Die :Resource wurde erstellt!"
}

And when we use this, we'd now get:

__("The :resource was created!", ['resource' => 'kategorie']) // "Die Kategorie wurde erstellt!"
https://james.brooks.page/blog/laravel-localization-case-tips
Implementing RICE in Trello
At Laravel we use Trello to store all of the ideas we come up with and also any suggestions that are sent to us.
Show full content

At Laravel we use Trello to store all of the ideas we come up with and also any suggestions that are sent to us.

Recently, Taylor shared an article about RICE with the team and we could immediately see the benefits. RICE is described as:

Simple prioritization for product managers

Essentially, by scoring four different factors, you can calculate a score that you use to determine how important the feature is.

Reach: how many people will this impact? (Estimate within a defined time period.)

Impact: how much will this impact each person? (Massive = 3x, High = 2x, Medium = 1x, Low = 0.5x, Minimal = 0.25x.)

Confidence: how confident are you in your estimates? (High = 100%, Medium = 80%, Low = 50%.)

Effort: how many “person-months” will this take? (Use whole numbers and minimum of half a month – don’t get into the weeds of estimation.)

Now, having recently planned for the arrival of our baby and wedding using Trello, I'm a self-described Trello nerd. When used properly, Trello can become a super-power, especially when you're using with Butler enabled.

After reading the article, I was immediately drawn to whether we could build this into Trello. If you haven't guessed already, the answer is yes, you can.

To implement this yourself, you must have the Butler and Custom Fields powerups available and enabled.

Custom Fields

Before we can calculate the RICE score, we need to be able to enter the individual scores. To do this, I created five fields:

  • Reach - This should be a Number field.
  • Impact - This should be a Dropdown field with the options; 3, 2, 1, 0.50, 0.25. I also coloured the options; green, yellow, orange, red and black.
  • Confidence - This should be a Dropdown field with the options; 100, 80, 50, 20. Again, I coloured these; green, yellow, orange and red.
  • Effort - This should be a Number field.
  • RICE Score - This should be a Number field. I also checked the Show field on front of card option so that it's quickly visible without needing to open the card.
Butler

We can now calculate the RICE score. To do this we use a custom Butler Card Button with an action that sets the custom field RICE Score to:

{{%Reach}}*{{%Impact}}*{{%Confidence}}/{{%Effort}}
Done!

And that's it, we can now see the RICE score for a task on the front of a card, just by providing the individual scores.

If you wanted to, you could take this further by using more of the features available in Butler, such as sorting by score, auto-labelling priorities based on the calculated RICE score and more!

I'd love to know if you're using this in your Trello boards, so please tweet me @jbrooksuk.

https://james.brooks.page/blog/implementing-rice-in-trello
Nova Customisable Resource Fields
This week I’m back working on Laravel Nova, which includes working my way through the nova-issues repo and seeing what’s what.
Show full content

This week I'm back working on Laravel Nova, which includes working my way through the nova-issues repo and seeing what's what.

One issue that caught my eye was a feature request from @Grayda, https://github.com/laravel/nova-issues/issues/2622. They asked that we provide functionality in Nova that allows users to show or hide fields based on their selection.

I immediately had a feeling this may be possible using the Filters feature of Nova, so I gave it a quick go and it worked!

Demo The Code

First thing's first, we start by creating a boolean filter:

php artisan nova:filter FieldsFilter --boolean

We use a boolean filter here so that we can display checkboxes to the user:

Filters

Now that we have our filter, we can start implementing the logic that we need. Within the options method, we define the array of fields that we want the user to be able to toggle:

/**
 * Get the filter's available options.
 *
 * @param  \Illuminate\Http\Request  $request
 * @return array
 */
public function options(Request $request)
{
    return [
        'Name' => 'name',
        'Gravatar' => 'gravatar',
        'Email' => 'email',
    ];
}

Note that with this method the options array uses a value/key syntax.

If we want to enable these fields by default, we'll also need to override the default method. You can do this by adding this code to your filter:

/**
 * Set the default options for the filter.
 *
 * @return array
 */
public function default()
{
    return [
        'name' => true,
        'gravatar' => true,
        'email' => true,
    ];
}

Once we've decided which fields the user should be allowed to toggle, we need to add the filter to our Resource. In the examples I've used above, we're modifying the User resource:

use App\Nova\Filters\FieldsFilter;

/**
 * Get the filters available for the resource.
 *
 * @param  \Illuminate\Http\Request  $request
 * @return array
 */
public function filters(Request $request)
{
    return [
        new FieldsFilter,
    ];
}

We're almost there, stay with me!

All we need to do now is toggle the field if the user wants to display it. Usually we'd use the fields method to describe the fields that you wish to display to a user. Since we only want to allow users to customise which fields are displayed on the index listing, we can use the fieldsForIndex method instead. This is a two part change. Firstly, we need to store the FieldsFilter in a variable so we can access it later:

/**
 * Get the fields displayed by the resource index.
 *
 * @param  \Illuminate\Http\Request  $request
 * @return array
 */
public function fieldsForIndex(Request $request)
{
    $fieldFilter = $request->filters()->first(function ($filter) {
        return $filter->filter instanceof FieldsFilter;
    });

    return [
        // Existing fields...
    ];
}

And finally, we need to modify our fields so that we hide it if the user no longer wants to see it:

return [
    ID::make()->sortable(),

    Gravatar::make('Gravatar')
            ->showOnIndex(Arr::get($fieldFilter->value, 'gravatar', true)),

    Text::make('Name')
        ->showOnIndex(Arr::get($fieldFilter->value, 'name', true)),

    Text::make('Email')
        ->sortable()
        ->showOnIndex(Arr::get($fieldFilter->value, 'email', true)),
];

Notice above how our fields don't contain any rules or help calls? This is because the index listing doesn't need to know any of that! We can make this method a lot cleaner than the usual fields method.

And we're done! Your users can now select which fields they want to display. You could take this further and have fields which are hidden by default, but then allow users to display them.

As a side note, you should be aware that each change will result in a fresh network request. This is required by Nova as changes to filters may result in a callback being handled differently, as demonstrated above with the use of $request->filters.

https://james.brooks.page/blog/nova-customisable-resource-fields
Creating Happiness Out Of Sadness
On the 30th October 2019, I launched Happy Dev a podcast in which I interview software developers and we discuss mental health.
Show full content

On the 30th October 2019, I launched Happy Dev a podcast in which I interview software developers and we discuss mental health. I see it as a platform in which we can share the guests' story; when they first became aware of their mental health, how it has manifested over the years and what we as an industry can do to help each other, among other things.

After my brother took his own life in 2018, I found myself at a bit of a loss. I'd lost someone very important to me and I didn't have an outlet for these strange and confusing feelings that I now had. I knew that I wanted to do something, but I wasn't sure what. Out of this came the idea of Happy Dev.

I found myself thinking about it more and more and eventually I settled on the idea of making a podcast. However, I kept putting it off because, in some ways, the idea of recording a podcast was embarrassing to me. It's silly in hindsight, but it's honestly how I felt at the time.

When I joined Laravel in July 2019, I was lucky to gain an audience of software developers and importantly, an audience of people who are likely to listen to podcasts. I'd also been listening to a lot of podcasts myself, which made me feel more positive about recording myself.

Around this time, I also realised that if I wanted to make a podcast, I could and I should do it. Nobody knew I had been thinking about it and so of course, nobody was going to encourage me to do it. I finally spoke to my wife, Katie about it and although she liked the idea of it, she suggested that I ask people in the industry what they think, so I did. I messaged the Laravel team about my idea and was honestly blown away by their overwhelming support and positivity towards it. So much so that Dries Vints was my first confirmed guest, before I'd even committed to making it.

With all of this positivity came the drive to commit myself to the idea and start making it.

I created a demo (I've always wanted to be in a band making demo tapes) and played it for Katie. She cried and I was worried that I'd made a huge mistake, but apparently, she was crying with pride, so that was a relief!

Here is the original demo in all its raw glory.

When listening to this, keep in mind that I recorded it using the tools I had available at the time which were; Beats Headphones microphone, GarageBand and some royalty-free music.

This was it, it was finally happening! I ordered a microphone and re-recorded the introduction, this time with some words I'd thought about and written down instead of making it up.

I'd like to point out that even if I wasn't in the fortunate position to buy a microphone on a whim of an idea, I could've still produced Happy Dev without it. The point to this is that just because you don't have "professional" tools available, you can still make it.

What Happened Next

Well, it's fair to say that I'm constantly blown away by the overwhelming love and support for the podcast. As of writing, I've already released seven episodes (one trailer and one bonus) and has accumulated over 1300 downloads.

I started with an incredibly strong line up and there's still other amazing guests coming, which is very exciting!

I'm now releasing an episode every other Wednesday, which just about gives me enough time to record, edit and publish in time. I was ahead for a while, but the Christmas break has messed that up a bit.

Sponsorship Disclosure

Although I've not yet confirmed any sponsorship slots, I want to be very clear about how it'll work upfront.

I cover all of the Happy Dev costs personally. The cost equates to about $40 / mo and is made up of:

  • $19.00 / mo for Transistor.fm. Transistor hosts the podcast, website and makes it easy to create a draft episode.
  • £10.00 ($13 ish) / mo for Epidemic Sound. They license the intro and outro music. I would love to either purchase the use of the sounds for the entire show or record my own.
  • $89.99 / yr for Hover.com. Hover registered the HappyDev.fm domain.

Should I be fortunate enough to have sponsorship on the show, then this is the deal:

  • I want to be able to cover the costs of the show.
  • The remainder of any money made from sponsorships will be split and divided to a few different mental health charities, from Happy Dev.
  • Sponsorship must be relevant to the show and audience. I will only place advertisements where I believe the company supports mental health positively.

So for example, if Happy Dev had sponsorship of $500, I would cover the $40 costs and donate the remaining $460 to charity.

The charities I have picked are:

  • Open Source Mental Health - Raising awareness and ending the stigma of mental illness in the developer/tech community.
  • Boys Get Sad Too - 20% of their profits actually go towards CALM, but I believe strongly in their message. BGST supply apparel with conversation provoking designs. Donations would go directly to BGST so that they can promote their message louder instead of purchasing apparel.
  • PAPYRUS - Provides suicide prevention support and a hotline. I've previously raised funds for PAPYRUS.

From time to time I may evaluate the list and add or remove charities.

My hope is that this post has inspired you in some way. If you've been thinking about starting something but been too scared or put off for some reason, my advice to you is that you go for it and see what comes from it!

If you haven't already, I'd love for you to give Happy Dev a listen, subscribe for future episodes and follow @HappyDevFM on Twitter.

Sponsor Happy Dev on Patreon

You can now sponsor Happy Dev on Patreon!

https://james.brooks.page/blog/creating-happiness-out-of-sadness
Tighten’s Jigsaw, GitHub Pages and GitHub Actions
How to configure Tighten Jigsaw to deploy to GitHub Pages with a custom GitHub Action.
Show full content

This blog was previously powered by Tighten’s Jigsaw project. Even though it’s no longer used, if you’re thinking about starting your own blog, I can still highly recommend it.

Since Jigsaw generates a static website, I chose to host it with GitHub Pages as a "user site" as they allow for top-level domains. GitHub Pages only serve static sites, but as Jigsaw is a dynamic system we need to generate the site. I was able to automate this process with GitHub Actions. It took some trial and error (read: a lot of wip commits) but I was finally able to make it work and wanted to share this setup as I think it’s pretty sweet.

I’ll break this down into two sections, the first, using Jigsaw and GitHub Pages and the second, publishing with GitHub Actions.

Jigsaw & GitHub Pages

Jigsaw provides instructions on how to use GitHub Pages, but it’s written for project sites and not user sites. If you’re not sure why this is a problem, it’s because user sites require that the site contents are in the master branch, whereas repository sites default to gh-pages - though that can be changed.

To get this working, I setup Jigsaw under a source branch which contains all of the code, assets and post content. I then push the contents of the build_production directory to the master branch, which GitHub happily serves up for you to read.

It’s a small change, but it may not immediately be obvious, so it’s worth clarifying!

Jigsaw & GitHub Actions

Jigsaw is really, really good but I’m lazy and don’t want to manually be building my website, committing and publishing it after each post.

Admittedly, it took me a lot of time to figure these steps out, but now I’m able to write a new post and have GitHub automatically build and publish it for me!

I created a new workflow under .github/workflows/build-publish.yml with the following:

name: Build & Publish

on:
  push:
    branches:
      - source
  schedule:
    - cron: "0 2 * * 1-5"

jobs:
  build-site:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - name: Install Composer Dependencies
      run: composer install --no-ansi --no-interaction --no-scripts --no-suggest --no-progress --prefer-dist
    - name: Install NPM Dependencies
      run: npm install
    - name: Build Site
      run: npm run production
    - name: Create CNAME File
      run: echo "james.brooks.page" >> build_production/CNAME
    - name: Stage Files
      run: git add -f build_production
    - name: Commit files
      run: |
        git config --local user.email "actions@github.com"
        git config --local user.name "GitHub Actions"
        git commit -m "Build for deploy"
    - name: Publish
      run: |
        git subtree split --prefix build_production -b master
        git push -f origin master:master
        git branch -D master

It may look complicated, but we can break it down.

  1. We tell GitHub to only run the workflow on pushes to the source branch. I also opted to automatically run the workflow at 2PM Monday - Friday, just in case 🤷🏻‍♂️
  2. Next we’re installing the Composer and NPM dependencies.
  3. Once the dependencies have been installed, we build the site using the production environment configuration.
  4. Optional! I’m using a CNAME on GitHub Pages so that I can use my own domain. I create a new file and fill it with the domain name I want to use.
  5. I forcefully stage the build_production directory, this is because Jigsaw defaults to ignoring build_* directories from Git and I don’t want to change this and accidentally publish it into the source branch.
  6. Next we configure Git’s email and name, I chose to use GitHub Actions as the author so that I know it’s an automatic commit. We then commit the contents with a pre-defined commit message.
  7. Finally, we’re going to publish the site by pushing the changes from within the GitHub Action itself! This is a bit complicated if you’re not confident with Git, but let’s try to keep it simple. We create a subtree of the build_production directory and pushing it into a temporary master branch. Now we can force push the branch to GitHub and clean up by deleting the master branch1.

And that’s it, I’m now able to write new blog posts without my laptop (using just the GitHub website) and have them immediately published for me.

Let me know @jbrooksuk if you’re using this technique or have any suggestions on how to improve it.

Footnotes
  1. Because we’re force-pushing to master from a branch which contains only one commit, all history is lost for the published website. This is a trade-off that I’m happy with because the real history is in the source directory.
https://james.brooks.page/blog/jigsaw-github-actions
Not My First Blog Post
We’re about to enter a new decade, so let’s start it with a new blog.
Show full content

Tomorrow brings with it a new year and the start of scribbling out 2019 and writing 2020 for at least the first 4 months.

I'm not cool enough to have a blog, so here is my year in review in Twitter form.

— James Brooks (@jbrooksuk) December 31, 2019

Earlier today I tweeted my reflection on 2019, starting with the fact that I'm not cool enough to have a blog. I'm a dad now, I need to earn back some cool points, so I thought what better way to start a new decade (yes, it's a new decade...) than to start it with a habit of blogging?

When I was in college, I used to write a blog and filled it with the things I had been learning about and news on projects I'd been working on. Nobody read it, but I felt better for getting my thoughts out. I guess, I treated it like a very nerdy diary.

I'd like to think that I'll fill this with the same kind of useful information, even if it's only useful to me. But of course, it's yet to be seen whether I post beyond today!

Hello World!

https://james.brooks.page/blog/my-first-blog-post
Laravel 4 & Dokku: Queue Workers
How we implemented Laravel 4’s Queue Component in our CRM system for improved email handling and UI responsiveness.
Show full content

As I mentioned in my last post we've been developing our new CRM system in Laravel 4 which has been great! One of the big features I'm particularly in love with is the Queue component. As we all know, a queue allows us to push code into a separate thread so that our main code will not block for as long. An example of where this is useful is Emailing.

One of the new features we have allows management to flag actions and ask staff for feedback on why they did something. This is really useful to gain a bit more information from them when they're unable to get in touch with a customer etc. When management click the flag buttons, we update the database & the UI to inform them that the action has succeeded, and we also queue up an email to the agent that alerts them that further feedback is required. Sending an email can take a couple of seconds, so we queue up this job and send it separately. In my testing I found that queuing reduced the time from 5-8 seconds down to 1s, depending on load and network speed etc. This obviously makes for a massive improvement on the client side as the UI is now a lot more responsive to actions.

Whilst I was developing this feature locally I was running php artisan queue:listen and then every time I flagged something, the email would be queued and I'd receive my email shortly afterwards.

I soon realised that I wouldn't be able to run the queue from our Dokku server, since it's just a PHP build pack with no extra or processes I can run. I could run the queue command on another server under supervisor but that would be lame and require me to run two copies of our project. No way!

If anyone has ever used Heroku they'll know that you can make use of a Procfile. These define what processes the server should run. Usually every app will use a web process, which is simply the main server code that handles your website. Another process type is the worker process. This will run alongside your web process and run the command forever.

Remembering this I figured that since Dokku is similar to Heroku I could do the same thing... It turns out that you can't. Dokku only supports the web process type and it took me a while to figure out what the process should even be running! Once I was able to override the default PHP buildpack behaviour using web: bin/run and get our application running again I simply tried adding: worker: php artisan queue:listen. Nadda. Our application was still up, but the queue wasn't being processed.

After a bit of digging I found that Dokku (and therefore Docker?) only supports the web process type, as I said before. My first thought was "oh god, this isn't going to end well...". Was that months of work wasted? Would I seriously have to maintain two copies of the app so that the worker could run elsewhere?

Thankfully not! There is a Dokku plugin that runs all process types! dokku-shoreman came to my rescue.

Once I'd installed the plugin:

git clone https://github.com/statianzo/dokku-shoreman.git /var/lib/dokku/plugins/dokku-shoreman

I pushed the code again, the queue job started being processed!

Voila!

Now I've got Dokku setup with dokku-shoreman and my Procfile looks like:

web: bin/run  
worker: php artisan queue:listen  

There is little to no information about this on the Internet, so hopefully this will be of use to someone!

https://james.brooks.page/blog/laravel-4-dokku-queue-workers