git config --global branch.sort -committerdate
Now, `git branch` sorts my branches by last commit date, most recent first.
To reset:
git config --global --unset branch.sort]]>
We have a PHPUnit test suite that's taking a while, so I wanted to find out the which tests were taking the longest.
With the help of Google, I discovered the --log-junit phpunit-results.xml command line option to phpunit which creates an XML file with the info I need. Running on Slim's codebase, we create the XML file with:
$ ./vendor/bin/phpunit --log-junit phpunit-results.xml
...
Tests: 436, Assertions: 832, PHPUnit Notices: 37.
It looks like this (formatted by me):
$ head -n 7 phpunit-results.xml
<?xml version="1.0" encoding="UTF-8"?>
<testsuites>
<testsuite
name="/Users/rob/Projects/slim/slimphp/Slim/phpunit.xml"
tests="436"
assertions="832"
errors="0"
failures="0"
skipped="0"
time="0.595106">
<testsuite
name="Slim Test Suite"
tests="436"
assertions="832"
errors="0"
failures="0"
skipped="0"
time="0.595106">
<testsuite
name="Slim\Tests\Routing\RouteContextTest"
file="/Users/rob/Projects/slim/slimphp/Slim/tests/Routing/RouteContextTest.php"
tests="5"
assertions="14"
errors="0"
failures="0"
skipped="0"
time="0.004264">
<testcase
name="testCanCreateInstanceWithoutRoute"
file="/Users/rob/Projects/slim/slimphp/Slim/tests/Routing/RouteContextTest.php"
line="43"
class="Slim\Tests\Routing\RouteContextTest"
classname="Slim.Tests.Routing.RouteContextTest"
assertions="4"
time="0.002566"/>
<testcase
name="testCanCreateInstanceWithoutBasePathAndThrowExceptionIfGetBasePathIsCalled"
file="/Users/rob/Projects/slim/slimphp/Slim/tests/Routing/RouteContextTest.php"
line="57"
class="Slim\Tests\Routing\RouteContextTest"
classname="Slim.Tests.Routing.RouteContextTest"
assertions="4"
time="0.000505"/>
Each testcase has a time attribute, so I asked my LLM to create some Python to sort by time and list the top 5:
$ python3 -I -c '
import sys, heapq, xml.etree.ElementTree as ET
cases = ET.parse(sys.argv[1]).iter("testcase")
slowest = heapq.nlargest(5, cases, key=lambda c: float(c.get("time", 0)))
for c in slowest:
print(f"{float(c.get("time", 0)):8.3f}s {c.get("class")}::{c.get("name")}")
' phpunit-results.xml
For this test, I get:
$ python3 -I -c 'import sys, heapq, xml.etree.ElementTree as ET; print(*(f"{float(c.get("time", 0)):8.3f}s {c.get("class")}::{c.get("name")}" for c in heapq.nlargest(5, ET.parse(sys.argv[1]).iter("testcase"), key=lambda c: float(c.get("time", 0)))), sep="\n")' phpunit-results.xml
0.028275 Slim\Tests\AppTest testAnyRoute
0.026482 Slim\Tests\AppTest testInvokeSequentialProcessAfterAddingAnotherRouteArgument
0.020176 Slim\Tests\AppTest testInvokeSequentialProcessToAPathWithOptionalArgsAndWithoutOptionalArgsAndKeepSetedArgs
0.016482 Slim\Tests\MiddlewareDispatcherTest testExecutesMiddlewareLastInFirstOut
0.013571 Slim\Tests\Factory\AppFactoryTest testCreateAppWithContainerUsesContainerDependenciesWhenPresent
Obviously, Slim's tests are quick, so there's no much to see here, but for my client's project, the numbers are larger!
]]>The OpenAPI track was excellent. We started with Frank Kilcommins covering API workflows before Lorna Mitchell talked about upgrading your OpenAPI spec to the latest 3.2. She really put it into perspective on how old version 3.0 is, and the benefits of updating to the latest version. Kin Lane followed with an interesting talk on OpenAPI extensions and how the help expand your specification, particularly when tied together with Overlays. He was followed by Chris Wood explaining the proposals around security that are in-flight at the moment.
There's a lot going on in the OpenAPI world and I will be paying more attention than I have. In particular, the security proposals look interesting and worth helping out on if I can. I also want to bring the OpenAPI story at one of my clients up to the latest version and see what we can leverage when we're there.
Another talk I was particularly happy to attend was Afsha H.'s talk on accountability in systems where agents work more autonomously. It's important that humans are responsible and do not hide behind "the agent did it", and Afsha's talk provided the things to think about. It was Afsha's first in-person technical conference talk and she knocked it out of the park.
In addition to the talks, the hallway track was good and I met new people starting out in the industry and caught up with old friends. The conversations were interesting and thought-provoking and I find it a very good way to learn new things from people who are actually doing and thinking about them.
It was fantastic to catch up with Phil Sturgeon too, which included a visit to the pub! Donate to his charity as he's doing amazing work.
If you're into APIs, I recommend attending a conference to find out where the industry is today. As Mehdi Medjaoui said at the start, conferences are where the humans connect (but we won't mention the SOAP joke!).

If I'm opening a directory, then I would like it to re-use the same window if I have previously opened that directory. However if I open a random file, I never want that file in the last directory window as it's unrelated.
To do this I wrote a simple bash function:
# subl: wrapper that opens files in a new window, and reuses the window
# for directories
subl() {
local args=() opts=()
for a in "$@"; do
if [[ "$a" == -* ]]; then
opts+=("$a")
else
args+=("$a")
fi
done
# No path argument means the current directory
[[ ${#args[@]} -eq 0 ]] && args=(.)
if [[ ${#args[@]} -eq 1 && -d "${args[0]}" ]]; then
# Single directory: open in same window
command subl "${opts[@]}" "${args[0]}"
else
# File(s): so open in a new window
command subl -n "${opts[@]}" "${args[@]}"
fi
}
Firstly we determine which parameters on the command line are arguments and which are options. Options start with a dash and arguments are either file or directory names.
After that, we inspect the arguments.
This works really well, but if I close a directory window and then open it again, it has forgotten which files I had open. To get Sublime Text to remember, we need a project file. This is a JSON file with the extension .sublime-project file and a minimal one would be:
{
"folders": [
{
"path": "."
}
]
}
We then open a directory with subl foo.sublime-project and then the open files are persisted across sessions.
Obviously, I'm not going to manually create this file or remember to use it as argument, so I extended my subl() function to manage the project file for me:
# subl: wrapper that opens files in a new window, and directories in the same
# window as a Sublime project, reusing an existing .sublime-project, creating
# one if opening a dir.
subl() {
local args=() opts=()
for a in "$@"; do
if [[ "$a" == -* ]]; then
opts+=("$a")
else
args+=("$a")
fi
done
# No path argument means the current directory
[[ ${#args[@]} -eq 0 ]] && args=(.)
if [[ ${#args[@]} -eq 1 && -d "${args[0]}" ]]; then
# Single directory: use existing project or create one, then open in same window
local proj
proj=$(find "${args[0]}" -maxdepth 1 -name '*.sublime-project' -print -quit)
if [[ -z "$proj" ]]; then
local name
name=$(basename "$(cd "${args[0]}" && pwd)")
proj="${args[0]}/${name}.sublime-project"
cat > "$proj" <<EOF
{
"folders": [
{
"path": ".",
"file_exclude_patterns": [".*.sublime-project", ".*.sublime-workspace"]
}
]
}
EOF
fi
command subl "${opts[@]}" "$proj"
else
# File(s): open in a new window
command subl -n "${opts[@]}" "${args[@]}"
fi
}
The comment covers what happens. Firstly we check for a sublime-project file. If one doesn't exist, then we create a default one called {directory name}.sublime-project. I'm not particularly interested in this project file, so I exclude it from the Sublime Text side bar using the file_exclude_patterns option within the project file itself. Then we open the project with the options that were provided on the command line.
Now subl . in a fresh directory creates a hidden project file and opens it, and every subl after that picks the project back up with my files open exactly where I left them.
]]>I set up locally like this:
and then when working on something, I tend to do these steps:
I have a couple of little irritations with this workflow that I finally sat down to solve.
To remove these minor irritations, I discovered that I can run these useful git commands:
Firstly, set main to track upstream/main:
git branch --set-upstream-to upstream/main main
Usually I set which repo's branch to track using git push -u, but that doesn't make sense for the main branch as I never push it to the upstream repo. Using --set-upstream-to on the branch command does the same thing, without a push.
Secondly, to make the first git push automatically create the origin/<branch> and track it:
git config push.autoSetupRemote true
Now git pull on main always pulls from upstream, and git push on a new branch just works.
]]>I quite like seeing the HTML rendering of an OpenAPI spec when reading it and understanding it, so I knocked up a simple shell script that takes an OpenAPI spec file and outputs an HTML doc. This is a trivial task thanks to Scalar!
This is the script:
~/bin/create-openapi-spec-doc.sh:
#!/usr/bin/env bash
set -eo pipefail
if [[ $# -eq 0 ]]; then
echo "Usage: $(basename "$0") <spec-file> [spec-file ...]"
echo "Generates an HTML API reference document for each OpenAPI spec file provided."
exit 1
fi
for spec_file in "$@"; do
base="${spec_file%.*}"
spec_name="$(basename "$base")"
{
cat <<EOF
<!doctype html>
<html>
<head>
<title>${spec_name} API</title>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1" />
</head>
<body>
<div id="app"></div>
<script
id="api-reference"
type="application/yaml">
EOF
cat "$spec_file"
cat <<'EOF'
</script>
<!-- Load the Script -->
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
</body>
</html>
EOF
} > "$base".html
echo "Generated: $base.html"
done
$ create-openapi-spec-doc.sh *.json
Generated: provided_svc.html
Generated: required_svc.html
and now I have nicely formatted HTML files!
]]>use http\Exception\InvalidArgumentException; // ... throw new InvalidArgumentException;
I was surprised as http\Exception\InvalidArgumentException is not a class in our system. While cooling, I discovered that there's an http PHP extension and it appears that PHPStan has a stub for this which means that it accepts it as existing even if it doesn't.
What we think happened is that I wrote throw new InvalidArgumentException; and selected the wrong class from the dropdown in PHPStorm as I wanted to import the base InvalidArgumentExtension
I want PHPStan to fail in this case, so I wrote a PHPStan extension that calls class_exists() to ensure that the class really is available:
<?php
declare(strict_types=1);
namespace Laminar\PHPStan\Rules;
use PhpParser\Node;
use PhpParser\Node\Stmt\Use_;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleError;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\ShouldNotHappenException;
/**
* Flags imported classes that PHPStan knows about via bundled extension stubs
* but cannot be loaded in the current runtime environment.
*
* This rule checks using the xxx_exists() functions, so is only useful if run within
* a context that matches deployment.
*
* @implements Rule<Use_>
*/
class ImportedClassExistsRule implements Rule
{
public function __construct(private readonly ReflectionProvider $reflectionProvider)
{
}
public function getNodeType(): string
{
return Use_::class;
}
/**
* @return list<RuleError>
* @throws ShouldNotHappenException
*/
public function processNode(Node $node, Scope $scope): array
{
if ($node->type !== Use_::TYPE_NORMAL) {
return [];
}
$errors = [];
foreach ($node->uses as $use) {
$className = $use->name->toString();
if (! $this->reflectionProvider->hasClass($className)) {
continue;
}
if (
class_exists($className)
|| interface_exists($className)
|| trait_exists($className)
|| enum_exists($className)
) {
continue;
}
$errors[] = RuleErrorBuilder::message(
sprintf(
'Imported class %s does not exist at runtime. The required PHP extension may not be installed.',
$className,
),
)->identifier('akrabat.importedClassExists')
->line($use->getLine())
->build();
}
return $errors;
}
}
The only important thing you need to know about writing a custom PHPStan rule is that processNode() returns an array of errors and if that is empty, then the rule succeeded.
This one isn't particularly complicated, but it solves the problem!
As an aside, to prevent PhpStorm from providing autocomplete for extensions that don't exist in your application, go to Settings→PHP and select the PHP Runtime tab. You can then select the extensions you use and deselect those that you don't. There's even a "Sync Extensions with Interpreter" button which can be handy if you build your dev container from your prod one.
]]>Navigating this to come out stronger rather than letting it consume my self-belief and confidence isn’t easy, especially at the time. I’m not worse at my job and my abilities haven’t diminished, but sometimes it’s hard to remember this.
I need to give myself space to remember who I am and internalise that my emotions are valid, but not the whole truth. Introspection and understanding of the whole is important.
Onwards and upwards.
]]>I have no words.

The lovely thing is that my photos remind me what happened during my year and I remember many good times through them as I've seen friends and family over the months. I've done for many years now and you can see them all in my Year in pictures category.
I started the year visiting the Severn Valley Railway and took some photos of steam trains. I also visited PHP Stoke which was fun.
I attended three community events in February, starting with FOSDEM at the very start, PHP UK mid-way through, and then Fusion towards the end. I really like catching up with the community and learning from them in the hallway track! I also photographed planes with Stuart!
I had a quick trip to Spain this month which was very cold and wet and then spoke at Drupal Mountain Camp; my first time attending this conference and I loved the community vibe here. I was also enjoyed my visit to the KVWR with Kevin.
In April, I went down to London to take care of my dad while my mum visited her sister in America. I fitted in a trip to the SVR Spring Gala, and also spoke at PHP Berkshire, sharing the bill with Dave Liddament
We celebrated lots of birthdays in May and visited Dudley Canal & Caverns. I spoke at phpday in Verona and later the PHP Oxford user group. The community at both events are fantastic and I enjoyed all the great conversations. My wife and I also enjoyed a visit to Charlecote. Most importantly, Crystal Palace won the FA cup this month for the first time in their history!
The Nintendo Switch 2 was released this month and it's a great sequel to the original Switch. We spent most of the month on holiday in Spain, where I ate lots of ice cream.
July was all about family and friends. My aunt and cousins from America visited, so we moved our family meet-up to July. We also had our regular lunch with friends in Crewe as that's half-way between us. We also celebrated our youngest's graduation of a first class degree in mathematics.
There was a big railway event in August celebrating 200 years of the modern railways. I visited mum & dad and I took dad out for breakfast at the local café which turned out to be the last time that dad and I will do that. We also visited our friend in Estonia.
The big news in September is that my dad's wellbeing deteriorated far enough that mum was no longer able to care for him. He was taken to hospital and is now living in a care home. I'm pleased that the transition seems to have gone smoothly for both of them. Later that month, I attended apidays London and then took a few days to take photos of trains in Yorkshire, while catching up with good friends in the evenings. The Moors really are a lovely place to spend time.
In October, my aunt from America visited again which was lovely as she stayed with my mum, arriving a few days after dad had moved into a care home. I also spoke at Nordic APIs Platform Summit in Stockholm. I was surprised to be on the main track and the conference as a whole was excellent. Finally, we celebrated another graduation as my wife graduated with a first class degree in Computing.
A quiet month, where I spoke at SymfonyCon which celebrated the 20th anniversary of the framework.
I finished the year attending apidays Paris, my first visit to this very large conference, where I met some amazing people and learned things. We had a lovely Christmas at home where lots of cake and mince pies were consumed. Then, right at the end of the month, I visited the SVR for the final time this year to take photos of heritage diesel locomotives. I also bought a new camera for next year!
Looking back over 2025, it was a bittersweet year with some excellent times with family, friends and community folks, but it was tinged with my dad's transition to a care home. As always, I also very much enjoyed my railway photography and trips with Stuart.
Who knows what 2026 will bring, but I will continue to take photos throughout!
]]>