Initial British Artisan package

This commit is contained in:
2026-07-12 21:40:40 +01:00
commit f3fa1f73af
9 changed files with 553 additions and 0 deletions
+20
View File
@@ -0,0 +1,20 @@
# Composer
/vendor/
composer.phar
# PHPUnit
.phpunit.cache/
.phpunit.result.cache
# IDEs
/.idea/
/.vscode/
# macOS
.DS_Store
# Logs
*.log
# Library dependency lock
/composer.lock
+23
View File
@@ -0,0 +1,23 @@
MIT License
Copyright (c) 2026 Seb Molenaar
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+96
View File
@@ -0,0 +1,96 @@
# British Artisan
British English command spellings for Laravel Artisan, with suitably dry corrections when the American spelling is used.
## What it does
British Artisan lets you use:
```bash
php artisan optimise
php artisan optimise:clear
```
Laravel still receives and runs its native commands:
```bash
php artisan optimize
php artisan optimize:clear
```
The British spelling runs silently. The American spelling still works, but receives a randomly selected reminder that the proper spelling exists.
## Requirements
- PHP 8.2 or newer
- Laravel 11, 12, or 13
## Installation
Install the package with Composer:
```bash
composer require sebs-space/british-artisan
```
Then run the one-off installer:
```bash
php artisan british-artisan:install
```
The installer updates the project-root `artisan` file so command-line arguments are translated before Laravel resolves the command.
## Usage
Use the British spelling as normal:
```bash
php artisan optimise
php artisan optimise:clear
```
Any command beginning with `optimise` is translated to the matching `optimize` command before Laravel runs it.
The original American spelling continues to work:
```bash
php artisan optimize:clear
```
It simply receives a polite correction first.
## Uninstalling
Restore the original Laravel `artisan` bootstrap with:
```bash
php artisan british-artisan:uninstall
```
Then remove the package:
```bash
composer remove sebs-space/british-artisan
```
## Supported command translations
| British English | Laravel command |
|---|---|
| `optimise` | `optimize` |
| `optimise:*` | `optimize:*` |
Additional command translations can be added in future releases.
## Planned improvements
- Publishable configuration
- Custom correction messages
- Additional British command spellings where real Artisan commands use American English
## Licence
British Artisan is open-source software licensed under the [MIT License](LICENSE).
+45
View File
@@ -0,0 +1,45 @@
{
"name": "sebs-space/british-artisan",
"description": "British English aliases for Laravel Artisan commands, with suitably dry corrections for American spellings.",
"type": "library",
"license": "MIT",
"authors": [
{
"name": "Seb Molenaar",
"email": "seb@sebs.space"
}
],
"require": {
"php": "^8.2",
"illuminate/console": "^11.0|^12.0|^13.0",
"illuminate/support": "^11.0|^12.0|^13.0"
},
"require-dev": {
"orchestra/testbench": "^9.0|^10.0|^11.0",
"phpunit/phpunit": "^11.0|^12.0"
},
"autoload": {
"psr-4": {
"SebsSpace\\BritishArtisan\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"SebsSpace\\BritishArtisan\\Tests\\": "tests/"
}
},
"extra": {
"laravel": {
"providers": [
"SebsSpace\\BritishArtisan\\BritishArtisanPackageServiceProvider"
]
}
},
"scripts": {
"test": "phpunit"
},
"minimum-stability": "stable",
"prefer-stable": true
}
+168
View File
@@ -0,0 +1,168 @@
<?php
namespace SebsSpace\BritishArtisan;
use RuntimeException;
class ArtisanPatcher
{
public const INSTALLED = 'installed';
public const ALREADY_INSTALLED = 'already_installed';
public const UNINSTALLED = 'uninstalled';
public const NOT_INSTALLED = 'not_installed';
private const IMPORT = 'use SebsSpace\\BritishArtisan\\BritishArtisanSpelling;';
private const ARG_INPUT_IMPORT = 'use Symfony\\Component\\Console\\Input\\ArgvInput;';
private const PATCHED_COMMAND = '$status = $app->handleCommand(new ArgvInput(BritishArtisanSpelling::prepareArguments($_SERVER[\'argv\'])));';
private const ORIGINAL_COMMANDS = [
'$status = $app->handleCommand(new ArgvInput);',
'$status = $app->handleCommand(new ArgvInput());',
];
public function __construct(private readonly string $artisanPath)
{
}
public function install(): string
{
$contents = $this->read();
if ($this->isInstalled($contents)) {
return self::ALREADY_INSTALLED;
}
if (! str_contains($contents, self::ARG_INPUT_IMPORT)) {
throw new RuntimeException(
'Could not find the expected ArgvInput import in the project-root artisan file. No changes were made.'
);
}
$originalCommand = $this->findOriginalCommand($contents);
if ($originalCommand === null) {
throw new RuntimeException(
'Could not find the expected handleCommand call in the project-root artisan file. No changes were made.'
);
}
$updated = str_replace(
self::ARG_INPUT_IMPORT,
self::IMPORT.PHP_EOL.self::ARG_INPUT_IMPORT,
$contents,
$importReplacements,
);
$updated = str_replace(
$originalCommand,
self::PATCHED_COMMAND,
$updated,
$commandReplacements,
);
if ($importReplacements !== 1 || $commandReplacements !== 1) {
throw new RuntimeException(
'The artisan file did not match the expected structure. No changes were made.'
);
}
$this->write($updated);
return self::INSTALLED;
}
public function uninstall(): string
{
$contents = $this->read();
if (! str_contains($contents, self::IMPORT) && ! str_contains($contents, self::PATCHED_COMMAND)) {
return self::NOT_INSTALLED;
}
if (! $this->isInstalled($contents)) {
throw new RuntimeException(
'The artisan file contains only part of the British Artisan installation. No changes were made.'
);
}
$updated = str_replace(
self::IMPORT.PHP_EOL,
'',
$contents,
$importReplacements,
);
$updated = str_replace(
self::PATCHED_COMMAND,
self::ORIGINAL_COMMANDS[0],
$updated,
$commandReplacements,
);
if ($importReplacements !== 1 || $commandReplacements !== 1) {
throw new RuntimeException(
'The artisan file did not match the expected British Artisan installation. No changes were made.'
);
}
$this->write($updated);
return self::UNINSTALLED;
}
private function isInstalled(string $contents): bool
{
return str_contains($contents, self::IMPORT)
&& str_contains($contents, self::PATCHED_COMMAND);
}
private function findOriginalCommand(string $contents): ?string
{
foreach (self::ORIGINAL_COMMANDS as $candidate) {
if (str_contains($contents, $candidate)) {
return $candidate;
}
}
return null;
}
private function read(): string
{
if (! is_file($this->artisanPath) || ! is_readable($this->artisanPath) || ! is_writable($this->artisanPath)) {
throw new RuntimeException('The project-root artisan file could not be read and written.');
}
$contents = file_get_contents($this->artisanPath);
if ($contents === false) {
throw new RuntimeException('The project-root artisan file could not be read.');
}
return $contents;
}
private function write(string $contents): void
{
$temporaryPath = $this->artisanPath.'.british-artisan.tmp';
try {
if (file_put_contents($temporaryPath, $contents, LOCK_EX) === false) {
throw new RuntimeException('Could not write the temporary artisan file.');
}
if (! rename($temporaryPath, $this->artisanPath)) {
throw new RuntimeException('Could not replace the artisan file.');
}
} catch (RuntimeException $exception) {
@unlink($temporaryPath);
throw $exception;
}
}
}
@@ -0,0 +1,25 @@
<?php
namespace SebsSpace\BritishArtisan;
use Illuminate\Support\ServiceProvider;
use SebsSpace\BritishArtisan\Console\BritishArtisanInstallCommand;
use SebsSpace\BritishArtisan\Console\BritishArtisanUninstallCommand;
class BritishArtisanPackageServiceProvider extends ServiceProvider
{
/**
* Bootstrap the package services.
*/
public function boot(): void
{
if (! $this->app->runningInConsole()) {
return;
}
$this->commands([
BritishArtisanInstallCommand::class,
BritishArtisanUninstallCommand::class,
]);
}
}
+100
View File
@@ -0,0 +1,100 @@
<?php
namespace SebsSpace\BritishArtisan;
class BritishArtisanSpelling
{
/**
* British spellings mapped to the command spellings Laravel expects.
* Add future command translations here.
*
* @var array<string, string>
*/
private const COMMAND_TRANSLATIONS = [
'optimise' => 'optimize',
];
/**
* Messages shown when American spellings are used.
* A random message is selected each time.
*
* @var array<int, string>
*/
private const AMERICAN_SPELLING_MESSAGES = [
'Refusing to acknowledge that spelling. The proper command is "%s". Running it anyway…',
'Detected simplified English. Translating to "%s"…',
'Converting from American to British English: "%s".',
'The Monarch would prefer "%s". Continuing…',
'Tea has been consulted. The correct command is "%s".',
'Correcting a minor transatlantic spelling incident: "%s".',
'British English engaged. Continuing with "%s".',
'Converting to proper English: "%s".',
'Applying the Oxford English Dictionary… "%s" selected.',
'The Oxford English Dictionary suggests "%s".',
'A translator has been dispatched. Continuing with "%s".',
'The command has been politely translated to "%s".',
'The Commonwealth appreciates "%s".',
'Keeping calm and carrying on with "%s".',
'The tea kettle whistled approvingly at "%s".',
'A brief linguistic correction has been applied: "%s".',
'Replacing American spelling with "%s".',
'Civilisation restored. Executing "%s".',
];
/**
* Prepare the Artisan arguments before Laravel resolves the command.
*
* @param array<int, string> $arguments
* @return array<int, string>
*/
public static function prepareArguments(array $arguments): array
{
$originalCommand = $arguments[1] ?? null;
if (is_string($originalCommand) && ($properCommand = self::properSpellingFor($originalCommand))) {
fwrite(
STDERR,
sprintf(
self::AMERICAN_SPELLING_MESSAGES[array_rand(self::AMERICAN_SPELLING_MESSAGES)],
$properCommand,
).PHP_EOL,
);
}
if (isset($arguments[1])) {
$arguments[1] = self::translateCommand($arguments[1]);
}
return $arguments;
}
public static function translateCommand(string $command): string
{
foreach (self::COMMAND_TRANSLATIONS as $british => $american) {
if ($command === $british) {
return $american;
}
if (str_starts_with($command, $british.':')) {
return $american.substr($command, strlen($british));
}
}
return $command;
}
public static function properSpellingFor(string $command): ?string
{
foreach (self::COMMAND_TRANSLATIONS as $british => $american) {
if ($command === $american) {
return $british;
}
if (str_starts_with($command, $american.':')) {
return $british.substr($command, strlen($american));
}
}
return null;
}
}
@@ -0,0 +1,38 @@
<?php
namespace SebsSpace\BritishArtisan\Console;
use Illuminate\Console\Command;
use RuntimeException;
use SebsSpace\BritishArtisan\ArtisanPatcher;
class BritishArtisanInstallCommand extends Command
{
protected $signature = 'british-artisan:install';
protected $description = 'Enable British English spellings for Laravel Artisan commands';
public function handle(): int
{
$patcher = new ArtisanPatcher(base_path('artisan'));
try {
$result = $patcher->install();
} catch (RuntimeException $exception) {
$this->error($exception->getMessage());
return self::FAILURE;
}
if ($result === ArtisanPatcher::ALREADY_INSTALLED) {
$this->info('British Artisan is already installed.');
return self::SUCCESS;
}
$this->info('British Artisan installed successfully.');
$this->line('You may now use commands such as: php artisan optimise:clear');
return self::SUCCESS;
}
}
@@ -0,0 +1,38 @@
<?php
namespace SebsSpace\BritishArtisan\Console;
use Illuminate\Console\Command;
use RuntimeException;
use SebsSpace\BritishArtisan\ArtisanPatcher;
class BritishArtisanUninstallCommand extends Command
{
protected $signature = 'british-artisan:uninstall';
protected $description = 'Disable British English spellings for Laravel Artisan commands';
public function handle(): int
{
$patcher = new ArtisanPatcher(base_path('artisan'));
try {
$result = $patcher->uninstall();
} catch (RuntimeException $exception) {
$this->error($exception->getMessage());
return self::FAILURE;
}
if ($result === ArtisanPatcher::NOT_INSTALLED) {
$this->info('British Artisan is not installed.');
return self::SUCCESS;
}
$this->info('British Artisan uninstalled successfully.');
$this->line('Laravel Artisan has been restored to its original command handling.');
return self::SUCCESS;
}
}