Posted: 2025-12-16 11:44:20 by Alasdair Keyes
In recent years the PHP testing tool Pest has gained much traction. So much so, the Laravel testing documentation shows test suite examples in Pest as well as PHPUnit.
On the whole I'm happy with PHPUnit, however when performing browser-based tests with Laravel I have had no end of problems with getting Laravel's Dusk to work within Docker.
Pest has built-in browser support so I was interested in seeing how well it worked and how easily the browser tests were to set up.
Pest is a superset of PHPUnit so if you want to use Pest, you can still keep some of your existing PHPUnit tests and just use Pest where you want. Alternatively Pest does provide a tool to migrate your PHPUnit tests to Pest.
For the examples below, I'm assuming that you are running your Laravel/Vite/JS development environment in a single container or Virtual Machine.
The following is a migration from PHPUnit to Pest.
# Remove PHPUnit
composer remove phpunit/phpunit --dev
# Install Pest basic suite
composer require pestphp/pest --dev --with-all-dependencies
./vendor/bin/pest --init
Once this is done you can run either pest or Laravel's test tool
vendor/bin/pest
# or
./artisan test
composer require pestphp/pest-plugin-browser:^4.0 --dev
npm install playwright@latest
npx playwright install
As mentioned, Pest will continue to run your PHPUnit tests without issue. But if you wish to switch to Pest fully, you can install the Drift plugin
composer require pestphp/pest-plugin-drift --dev
Then convert your tests....
$ ./vendor/bin/pest --drift
✔✔✔✔✔✔✔✔✔✔✔.
INFO The [tests] directory has been migrated to PEST with 11 files changed.
After you have committed your changes, you can remove the drift plugin
composer remove pestphp/pest-plugin-drift --dev
Now that we have Pest configured, it's worth checking it's working. Here's a quick test on the Laravel welcome page that comes with a fresh install.
<?php
test('Site gives 200 response', function () {
$response = $this->get('/');
$response->assertStatus(200);
});
test('Site produces correct output', function () {
$page = visit('/')
->on()->desktop()
->inDarkMode();
$page->assertSee('Laravel');
});
and run it...
$ vendor/bin/pest tests/Feature/PestExampleTest.php
PASS Tests\Feature\PestExampleTest
✓ Site gives 200 response 0.13s
✓ Site produces correct output 1.86s
Tests: 2 passed (2 assertions)
Duration: 3.00s
In development I run Laravel in the php:fpm-x.x Docker image with npm installed through apt for the Javascript/Vite functionality. The following snippet is the changes to make to Dockerfile.
# Existing docker config...
# Begin requirements for Pest/Playwright
RUN apt install -y libxdamage1 libgtk-3-0t64 libpangocairo-1.0-0 \
libpango-1.0-0 libatk1.0-0t64 libcairo-gobject2 libcairo2 \
libasound2t64 libgstreamer1.0-0 libgstreamer1.0-0 libgstreamer1.0-dev \
libgtk-4-1 libgraphene-1.0-0 libxslt1.1 libwoff1 libvpx9 \
libevent-2.1-7t64 libopus0 libgcrypt20 libgpg-error0 \
libgstreamer-plugins-base1.0-0 libgstreamer1.0-0 libgstreamer-gl1.0-0 \
libgstreamer-plugins-bad1.0-0 libgstreamer-plugins-base1.0-0 libflite1 \
libwebpdemux2 libjxl0.11 libavif16 libharfbuzz-icu0 libwebpmux3 \
libenchant-2-2 libsecret-1-0 libhyphen0 libmanette-0.2-0 libx264-164 \
libx264-dev libnspr4 libnss3
RUN docker-php-ext-install sockets
# End requirements for Pest/Playwright
RUN composer install
RUN npm install
# Begin install Playwright for Pest browser testing
RUN npx playwright install
# End install Playwright for Pest browser testing
# Existing docker config...
If you found this useful, please feel free to donate via bitcoin to 1NT2ErDzLDBPB8CDLk6j1qUdT6FmxkMmNz
Posted: 2025-10-30 08:21:02 by Alasdair Keyes
I've recently updated my dev laptop to Debian Trixie. I waited a while as Debian hadn't ported VirtualBox to their Backports repository and Oracle hadn't released a version that would install correctly on Trixie.
Once Oracle finally released the .deb package for VirtualBox I rebuilt my machine only to get the following error upon starting a VM...
VT-x is being used by another hypervisor (VERR_VMX_IN_VMX_ROOT_MODE)
VirtualBox can't operate in VMX root mode. Please disable the KVM kernel extension, recompile your kernel and reboot.
The message is fairly self explanatory as to the problem. It looks like VirtualBox now doesn't like to share hypervisor duties.
VirtualBox is detecting that the KVM kernel modules are loaded and throwing the error. You can get VirtualBox VMs started by unloading the KVM Kernel modules
$ lsmod | grep kvm
kvm_intel 413696 0
kvm 1396736 1 kvm_intel
irqbypass 12288 1 kvm
$ sudo rmmod kvm_intel kvm
$ lsmod | grep kvm
$
Note: If you run AMD processors, you'll need to adjust the kvm_intel to kvm_amd.
Also Note: Loading and unloading is order-specific, you need to put the architecture specific kvm_xxx module before the standard kvm module.
I have created a couple of aliases in my ~/.bashrc file to load the KVM modules if I want by running load-kvm-modules and unload them with unload-kvm-modules
alias unload-kvm-modules="sudo rmmod kvm_intel kvm"
alias load-kvm-modules="sudo modprobe kvm_intel kvm"
I use VirtualBox far more than KVM so the simple fix is that I stop the KVM modules being loaded at boot by blacklisting the KVM modules. I created a new file called /etc/modprobe.d/no-kvm.conf with the following contents.
blacklist kvm_intel
blacklist kvm
If you use KVM more than VirtualBox, the modules are already present at boot, you can just use KVM as normal. You don't need to do anything other than create the same aliases as above to load/unload the modules when required.
If you found this useful, please feel free to donate via bitcoin to 1NT2ErDzLDBPB8CDLk6j1qUdT6FmxkMmNz
Posted: 2025-07-25 08:05:10 by Alasdair Keyes
It's Sysadmin day 2025.
Give thanks to your sysadmin. If you are a sysadmin, have a beer.
If you found this useful, please feel free to donate via bitcoin to 1NT2ErDzLDBPB8CDLk6j1qUdT6FmxkMmNz
Posted: 2025-07-05 13:02:41 by Alasdair Keyes
Often, if I'm updating an old code base, or have installed a new instance of Laravel, I want to enable PHP's strict typing.
This has to be enabled on each file and can be a bit of a pain so I knocked up a bash one-liner to do this quickly. It works recursively for any *.php file in a given directory.
find codebase/ -name *.php \
| xargs -i grep -L '^\s*declare\s*(\s*strict_types\s*=\s*1\s*)\s*;.*$' '{}' \
| xargs -i sed -i 's/\(<\?php\)/\1\n\ndeclare(strict_types=1);/' '{}'
Because blindly running random BASH commands from the internet is a bad idea, a quick break-down of each line....
codebase/ directory for every .php filedeclare(strict_types=1); declaration<?php tag, add the strict type declaration after the <?php tag leaving one blank line between the two.The command is idempotent, so re-running it will not add duplicate strict type declarations into your files.
A word of advice, be selective on which folder you execute it on. If you run it on the root of your code base and have a vendor/ folder, it will update all the PHP files within.
If you found this useful, please feel free to donate via bitcoin to 1NT2ErDzLDBPB8CDLk6j1qUdT6FmxkMmNz
Posted: 2025-04-14 21:05:39 by Alasdair Keyes
I've been running a mail server for about 20 years. For years I was using Exim (https://www.exim.org/) but due to one-too-many high security CVEs I decided to switch to Postfix (https://www.postfix.org/) about 2 years ago.
One thing that any mail server admin will attest to is that your server is constantly probed by bots trying to access mailboxes and send emails.
I store mail logs for ten weeks and in that time over half a million failed login attempts.
# zgrep 'SASL LOGIN authentication failed' /var/log/mail.log* | wc -l
570813
Even with logrotate and compressed logs, this is starting to take up a fair amount of disk space, mainly through the un-compressed logs before logrotate starts zipping them up.
I decided to install fail2ban (https://github.com/fail2ban/fail2ban) to kurb this. fail2ban is a tool that scans logs in real-time for lines that indicate probes/attacks and ban the IPs using the firewall. In their terminology a filter scans the logs for lines indicating an attack and a jail takes action of some kind to block the IPs.
I used to use fail2ban years ago when I first started managing servers, but stopped using it well over a decade ago as I wasn't finding it too much help at the time.
After installation, noticed that my specific setup with Postfix using saslauthd for SMTP authentication is not captured by the standard fail2ban Postfix filter.
A standard line in my logfile looks like...
2025-04-10T15:49:08.591090+01:00 myhostname postfix/smtpd[55089]: warning: unknown[1.2.3.4]: SASL LOGIN authentication failed: authentication failure, sasl_username=probedemail@domain.com
Instead of trying to bodge the existing filter/jail, I decided I'd write my own to detect just these lines. With a bit of trial and error I came up with the following two config files...
/etc/fail2ban/filter.d/custom-postfix-auth.conf
# Fail2Ban filter for Postfix with SASL auth failures
#
#
[Definition]
failregex = warning:\s+\S+\[<HOST>\]: SASL [A-Z\-\d]+ authentication failed: authentication failure, sasl_username=.*$
ignoreregex =
journalmatch = _SYSTEMD_UNIT=postfix@-.service
datepattern = {^LN-BEG}Epoch
{^LN-BEG}
# Author: Alasdair Keyes
/etc/fail2ban/jail.d/custom-postfix-auth.conf
[custom-postfix-auth]
enabled = true
maxretry = 30
bantime = 86400
backend = systemd
findtime = 86400
banaction = iptables-allports
Once these were in place I ran a test, this shows that it does indeed detect 64383 entries in the current log file.
# fail2ban-regex systemd-journal /etc/fail2ban/filter.d/custom-postfix-auth.conf
Running tests
=============
Use failregex filter file : custom-postfix-auth, basedir: /etc/fail2ban
Use datepattern : {^LN-BEG}Epoch
{^LN-BEG} : Default Detectors
Use systemd journal
Use encoding : UTF-8
Use journal match : _SYSTEMD_UNIT=postfix@-.service
Results
=======
Failregex: 9562 total
|- #) [# of hits] regular expression
| 1) [9562] warning:\s+\S+\[<HOST>\]: SASL [A-Z\-\d]+ authentication failed: authentication failure, sasl_username=.*$
`-
Ignoreregex: 0 total
Date template hits:
Lines: 44414 lines, 0 ignored, 9562 matched, 34852 missed
[processed in 2.96 sec]
Missed line(s): too many to print. Use --print-all-missed to print all 34852 lines
I reloaded fail2ban systemctl reload fail2ban and can now see I'm starting to block those troublesome IPs.
# fail2ban-client status custom-postfix-auth
Status for the jail: custom-postfix-auth
|- Filter
| |- Currently failed: 8
| |- Total failed: 9
| `- Journal matches: _SYSTEMD_UNIT=postfix@-.service
`- Actions
|- Currently banned: 3
|- Total banned: 3
`- Banned IP list: 1.2.3.4 40.50.60.70 6.7.8.9
If you found this useful, please feel free to donate via bitcoin to 1NT2ErDzLDBPB8CDLk6j1qUdT6FmxkMmNz
Posted: 2025-03-31 16:46:32 by Alasdair Keyes
I've just found out that Diggnation has been relaunched after it ended back in 2011. It's twelve episodes in and feels much like it was before... albeit with more grey hair.
Back in the mid-2000s when I first started working in tech, and browsing https://www.digg.com/; Diggnation was a regular watch on Saturday mornings with a cup of tea and a bowl of cereal. It was probably the first podcast I saw. This was in a time where podcasts weren't really a 'thing'.
Long may it continue.
Watch it at https://www.diggnation.show/.
If you found this useful, please feel free to donate via bitcoin to 1NT2ErDzLDBPB8CDLk6j1qUdT6FmxkMmNz
Posted: 2025-01-26 12:31:00 by Alasdair Keyes
I've recently heard some good things about the PHP tool Rector (https://getrector.com/). It provides automatic refactoring for your PHP code to increase code quality.
This website (https://www.akeyes.co.uk) is running on PHP 8.x, previously 7.x and with a quick check of my composer.json in Git, I can see that it was even running on PHP 5.6 when the site was built using the Slim Framework (https://www.slimframework.com/).
The earliest version of Laravel that I used was Laravel 5.5 on PHP 7.x, given that it's now on Laravel 11 on PHP 8.2, the code has progressed through many changes to the core language.
During this time, I've tried to keep the code updated. Implementing strict type checking on function parameters and return values. Keeping up with modern standards, but there will always bee a few bits I've missed. I thought this would be an ideal time to try Rector and see how it performs.
First, I checked out the latest version of the codebase on my dev machine and created a new branch.
I've tried to keep my test suite up-to-date which should give me some confidence that any changes made by Rector have not broken anything. We'll see how this confidence holds....
$ ./artisan test --compact
.............................................
Tests: 46 passed (140 assertions)
Duration: 1.50s
$
Before any changes, everything is passing.
$ composer require rector/rector --dev
./composer.json has been updated
Running composer update rector/rector
...
...
...
Using version ^2.0 for rector/rector
$
Rector is configured with a file called rector.php. If you don't have this, running Rector for the first time will detect this and offer to put in a basic config.
$ vendor/bin/rector
No "rector.php" config found. Should we generate it for you? [yes]:
> yes
[OK] The config is added now. Re-run command to make Rector do the work!
Taking a look at the file, it looks to have scanned for the common directories in my repo.
<?php
declare(strict_types=1);
use Rector\Config\RectorConfig;
return RectorConfig::configure()
->withPaths([
__DIR__ . '/app',
__DIR__ . '/bootstrap',
__DIR__ . '/config',
__DIR__ . '/lang',
__DIR__ . '/public',
__DIR__ . '/resources',
__DIR__ . '/routes',
__DIR__ . '/tests',
])
// uncomment to reach your current PHP version
// ->withPhpSets()
->withTypeCoverageLevel(0)
->withDeadCodeLevel(0)
->withCodeQualityLevel(0);
I'm going to keep it at the defaults for the time being, the only change I'll make is to uncomment ->withPhpSets(), this will cause Rector to check the PHP version in my composer.json file and test against that (Currently PHP 8.2).
Running rector again outputs it's suggested changes and has updated the PHP files.
$ vendor/bin/rector
136/136 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
12 files with changes
=====================
...
... (Remove for brevity)
...
[OK] 12 files have been changed by Rector
$
Right off the bat, let's see if anything's broken...
$ ./artisan test --compact
.............................................
Tests: 46 passed (140 assertions)
Duration: 1.99s
$
So far, so good. Let's look at some of the changes it's suggested.
Firstly the RemoveExtraParametersRector option has detected that the optional parameters for PHPUnit's assertStatus() can be removed.
- $response->assertStatus(200, "Assert $uri returns a 200 response");
+ $response->assertStatus(200);
It looks like I added this second parameter to add some debug when I refactored the tests after upgrading to PHPUnit 10.x and hit some problems. It looks like I got confused with Perl's test suite which allows extra parameters. Checking the docs, the assertStatus() function does only accept a single parameter. +1 for Rector
bootstrap/cache.As the path suggests, the files bootstrap/cache/services.php and bootstrap/cache/packages.php are cache files and will be deleted and recreated regularly by Laravel. There is no need to fix these. This is user error and can be resolved by adding the following to my rector.php config file.
->withSkip([
__DIR__ . '/bootstrap/cache',
])
This is a good catch and can be removed. I probably left it there during debug and didn't remove it. +2 for Rector.
try {
$record = $reader->city($ipAddress);
- } catch (AddressNotFoundException $e) {
+ } catch (AddressNotFoundException) {
//
}
The AddClosureVoidReturnTypeWhereNoReturnRector option found a few instances in the boilerplate Laravel code within routes/console.php where no return value was specified on closures. +3 for Rector.
-Artisan::command('inspire', function () {
+Artisan::command('inspire', function (): void {
$this->comment(Inspiring::quote());
})->purpose('Display an inspiring quote');
The ClosureToArrowFunctionRector wishes me to convert anonymous functions to the newer format arrow => functions.
-Broadcast::channel('App.Models.User.{id}', function ($user, $id) {
- return (int) $user->id === (int) $id;
-});
+Broadcast::channel('App.Models.User.{id}', fn($user, $id) => (int) $user->id === (int) $id);
I actually prefer the old style anonymous function. Retaining the function () { ... } format helps my brain easily parse what the function is up to. The arrow format feels like a run-on sentence in my mind.
Although not an issue in this instance, arrow functions include variables from the parent scope and don't require the use keyword to pass variables e.g. function() use ($i) { ... }. Requiring use to bring variables into scope is a good thing in my mind, so I think I'll keep these functions as they are.
To stop this I will just add the following to my rector.php config file.
->withSkip([
\Rector\Php74\Rector\Closure\ClosureToArrowFunctionRector::class,
]);
Note: The documentation shows that you can use the non-qualified class name ClosureToArrowFunctionRector::class, however this give an error that the Rector rule does not exist, it may work for some rules, but for this one and possible others, you must give the fully qualified class name to the withSkip() function.
I like this. The ClassPropertyAssignToConstructorPromotionRector realises that as I'm now running PHP 8, the old style boiler plate class code can be dispensed with and the cleaner constructor property promotion format can be used. This was the only class in my codebase that was affected, but on a large codebase with some big classes this could really slim down the code. +4 for Rector.
@@ @@
class Repository
{
- /** @var string $folderPath */
- private $folderPath;
-
- /** @var string $gitBinary */
- private $gitBinary;
-
private const GIT_BINARY = 'git';
private const DEFAULT_TRUNCATED_COMMIT_ID_LENGTH = 10;
@@ @@
*
* @param string $folderPath
*/
- public function __construct(string $folderPath = '.', string $gitBinary = self::GIT_BINARY)
+ public function __construct(private string $folderPath = '.', private string $gitBinary = self::GIT_BINARY)
{
- $this->folderPath = $folderPath;
- $this->gitBinary = $gitBinary;
}
/**
Overall, I'm very impressed with Rector. It did nothing wrong, it broke no code and the only changes I had to make were due to my tastes rather than to fix a problem. If anything I wish my codebase was in a worse condition so that I could get more benefit from it.
It appears there's even a PHPStorm/Jetbrains plugin to automatically run Rector on your code base https://plugins.jetbrains.com/plugin/19718-rector-support.
If I took on an old codebase as part of a new role, I would definitely use Rector to incrementally modernise the code.
If you found this useful, please feel free to donate via bitcoin to 1NT2ErDzLDBPB8CDLk6j1qUdT6FmxkMmNz
Posted: 2025-01-03 10:00:35 by Alasdair Keyes
My most enjoyed albums of 2024...
Special mention to Squarepusher - Dostrotime.
If you found this useful, please feel free to donate via bitcoin to 1NT2ErDzLDBPB8CDLk6j1qUdT6FmxkMmNz
Posted: 2024-11-20 15:13:09 by Alasdair Keyes
After my blog post earlier this year about installing LineageOS onto an old OnePlus2 phone (https://www.akeyes.co.uk/blog/installing_lineageos_on_ancient_device); I've had some interest from people asking me to install LineageOS onto their older phones.
Installation of LineageOS is quite a technical endeavour and can be quite difficult for some devices. It seems that regular phone stores on the high street may offer hardware repairs but not Operating System installs.
As such I have expanded some of the services that I offer through my company Cloudee LTD (https://www.cloudee.co.uk/), to include installation of LineageOS onto old mobile devices.
So if you do not feel comfortable doing it yourself or have perhaps already tried and been unsuccessful, feel free to contact Cloudee and we'll see if we can help.
If you found this useful, please feel free to donate via bitcoin to 1NT2ErDzLDBPB8CDLk6j1qUdT6FmxkMmNz
Posted: 2024-07-26 10:50:42 by Alasdair Keyes
Once again, it's Sysadmin day.
Give thanks to your sysadmin. If you are a sysadmin, have a beer.
If you found this useful, please feel free to donate via bitcoin to 1NT2ErDzLDBPB8CDLk6j1qUdT6FmxkMmNz
© Alasdair Keyes
I'm now available for IT consultancy and software development services - Cloudee LTD.
Happy user of Digital Ocean (Affiliate link)