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