Initial British Artisan package
This commit is contained in:
+20
@@ -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
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user