|
| 1 | +--- |
| 2 | +name: composer-command |
| 3 | +description: Creates a new Composer command extending BaseCommand in src/Command/, registers it in CommandProvider.php, and scaffolds a matching PHPUnit test. Use when user says 'add command', 'new composer command', 'create myadmin command'. Do NOT use for modifying existing commands or non-command classes. |
| 4 | +--- |
| 5 | +# Composer Command |
| 6 | + |
| 7 | +Create new Composer CLI commands for the MyAdmin plugin installer, following the existing `BaseCommand` pattern in `src/Command/`. |
| 8 | + |
| 9 | +## Critical |
| 10 | + |
| 11 | +- Command name MUST follow the `myadmin:<kebab-case-name>` convention (e.g., `myadmin:rebuild-cache`). The only exception is the base `myadmin` command in `Command.php`. |
| 12 | +- Every new command class MUST be registered in `src/CommandProvider.php` — both as a `use` import and as a `new ClassName()` entry in the `getCommands()` array. |
| 13 | +- The `CommandProviderTest.php` test that asserts the command count MUST be updated to match the new total. |
| 14 | +- All command classes use `protected function execute(...)` returning `int` (0 for success). |
| 15 | +- Namespace is always `MyAdmin\Plugins\Command`. |
| 16 | + |
| 17 | +## Instructions |
| 18 | + |
| 19 | +1. **Choose the command name and class name.** |
| 20 | + - Command name: `myadmin:<kebab-case-verb-noun>` (e.g., `myadmin:clear-cache`) |
| 21 | + - Class name: PascalCase matching the command (e.g., `ClearCache`) |
| 22 | + - Verify the name doesn't conflict with existing commands: `Command`, `Parse`, `CreateUser`, `UpdatePlugins`, `SetPermissions`. |
| 23 | + |
| 24 | +2. **Create the command class at `src/Command/<ClassName>.php`.** |
| 25 | + Use this exact template: |
| 26 | + |
| 27 | + ```php |
| 28 | + <?php |
| 29 | + /** |
| 30 | + * Plugins Management |
| 31 | + * @author Joe Huss <detain@interserver.net> |
| 32 | + * @copyright 2025 |
| 33 | + * @package MyAdmin |
| 34 | + * @category Plugins |
| 35 | + */ |
| 36 | + |
| 37 | + namespace MyAdmin\Plugins\Command; |
| 38 | + |
| 39 | + use Symfony\Component\Console\Input\InputInterface; |
| 40 | + use Symfony\Component\Console\Output\OutputInterface; |
| 41 | + use Composer\Command\BaseCommand; |
| 42 | + |
| 43 | + /** |
| 44 | + * Class <ClassName> |
| 45 | + * |
| 46 | + * @package MyAdmin\Plugins\Command |
| 47 | + */ |
| 48 | + class <ClassName> extends BaseCommand |
| 49 | + { |
| 50 | + protected function configure() |
| 51 | + { |
| 52 | + $this |
| 53 | + ->setName('myadmin:<kebab-name>') |
| 54 | + ->setDescription('<Short description of what the command does>') |
| 55 | + ->setHelp('<Longer help text explaining usage>'); |
| 56 | + } |
| 57 | + |
| 58 | + protected function execute(InputInterface $input, OutputInterface $output): int |
| 59 | + { |
| 60 | + // Command logic here |
| 61 | + |
| 62 | + return 0; |
| 63 | + } |
| 64 | + } |
| 65 | + ``` |
| 66 | + |
| 67 | + - If the command needs arguments, add `use Symfony\Component\Console\Input\InputArgument;` and chain `->addArgument('name', InputArgument::REQUIRED, 'Description.')` in `configure()`. |
| 68 | + - If the command needs options, add `use Symfony\Component\Console\Input\InputOption;` and chain `->addOption(...)` in `configure()`. |
| 69 | + - Only include `initialize()` and `interact()` methods if the command actually needs them. The minimal pattern (see `SetPermissions.php`) omits them. |
| 70 | + - Verify the file exists and has correct namespace before proceeding. |
| 71 | + |
| 72 | +3. **Register the command in `src/CommandProvider.php`.** |
| 73 | + - Add a `use` import: `use MyAdmin\Plugins\Command\<ClassName>;` |
| 74 | + - Add `new <ClassName>()` to the end of the array in `getCommands()`. |
| 75 | + - Verify the import is alphabetically consistent with existing imports and the array entry is added. |
| 76 | + |
| 77 | +4. **Create the test file at `tests/Command/<ClassName>Test.php`.** |
| 78 | + Follow this template matching the project's ReflectionClass-based testing pattern: |
| 79 | + |
| 80 | + ```php |
| 81 | + <?php |
| 82 | + |
| 83 | + namespace Tests\MyAdmin\Plugins\Command; |
| 84 | + |
| 85 | + use MyAdmin\Plugins\Command\<ClassName>; |
| 86 | + use PHPUnit\Framework\TestCase; |
| 87 | + use ReflectionClass; |
| 88 | + |
| 89 | + /** |
| 90 | + * Test suite for the <ClassName> command class. |
| 91 | + * |
| 92 | + * Tests class structure and command configuration. |
| 93 | + * |
| 94 | + * @covers \MyAdmin\Plugins\Command\<ClassName> |
| 95 | + */ |
| 96 | + class <ClassName>Test extends TestCase |
| 97 | + { |
| 98 | + public function testExtendsBaseCommand(): void |
| 99 | + { |
| 100 | + $ref = new ReflectionClass(<ClassName>::class); |
| 101 | + $this->assertSame('Composer\Command\BaseCommand', $ref->getParentClass()->getName()); |
| 102 | + } |
| 103 | + |
| 104 | + public function testCommandNameIsMyadmin<PascalName>(): void |
| 105 | + { |
| 106 | + $command = new <ClassName>(); |
| 107 | + $this->assertSame('myadmin:<kebab-name>', $command->getName()); |
| 108 | + } |
| 109 | + |
| 110 | + public function testCommandHasDescription(): void |
| 111 | + { |
| 112 | + $command = new <ClassName>(); |
| 113 | + $this->assertNotEmpty($command->getDescription()); |
| 114 | + } |
| 115 | + |
| 116 | + public function testCommandHasHelp(): void |
| 117 | + { |
| 118 | + $command = new <ClassName>(); |
| 119 | + $this->assertNotEmpty($command->getHelp()); |
| 120 | + } |
| 121 | + |
| 122 | + public function testExecuteIsProtected(): void |
| 123 | + { |
| 124 | + $ref = new ReflectionClass(<ClassName>::class); |
| 125 | + $method = $ref->getMethod('execute'); |
| 126 | + $this->assertTrue($method->isProtected()); |
| 127 | + } |
| 128 | + } |
| 129 | + ``` |
| 130 | + |
| 131 | + - If the command has arguments, add tests for each argument (see `CreateUserTest.php` for the pattern: `testCommandHas<Arg>Argument`, `test<Arg>ArgumentIsRequired`, `test<Arg>ArgumentHasDescription`). |
| 132 | + - Verify the test file exists before proceeding. |
| 133 | + |
| 134 | +5. **Update `tests/CommandProviderTest.php`.** |
| 135 | + - Update the `testGetCommandsReturns*Commands` test: change the `assertCount` value from the current count to current + 1. |
| 136 | + - Add a new test method for the new command instance: |
| 137 | + ```php |
| 138 | + public function testContains<ClassName>Instance(): void |
| 139 | + { |
| 140 | + $provider = new CommandProvider(); |
| 141 | + $commands = $provider->getCommands(); |
| 142 | + $this->assertInstanceOf(\MyAdmin\Plugins\Command\<ClassName>::class, $commands[<new_index>]); |
| 143 | + } |
| 144 | + ``` |
| 145 | + - The index is the position in the `getCommands()` array (0-based). Currently indices 0-4 are used, so the next is 5. |
| 146 | + - Verify the count assertion matches the actual number of commands. |
| 147 | + |
| 148 | +6. **Run tests to verify everything passes.** |
| 149 | + ```bash |
| 150 | + vendor/bin/phpunit tests/Command/<ClassName>Test.php |
| 151 | + vendor/bin/phpunit tests/CommandProviderTest.php |
| 152 | + ``` |
| 153 | + - All tests must pass. If `CommandProviderTest` fails on count, check that you updated both the array in `CommandProvider.php` and the count assertion in the test. |
| 154 | + |
| 155 | +## Examples |
| 156 | + |
| 157 | +**User says:** "Add a new composer command called rebuild-cache that clears and rebuilds the plugin cache" |
| 158 | + |
| 159 | +**Actions taken:** |
| 160 | +1. Create `src/Command/RebuildCache.php` with class `RebuildCache extends BaseCommand`, command name `myadmin:rebuild-cache`. |
| 161 | +2. Add `use MyAdmin\Plugins\Command\RebuildCache;` and `new RebuildCache()` to `src/CommandProvider.php`. |
| 162 | +3. Create `tests/Command/RebuildCacheTest.php` with tests for: extends BaseCommand, command name, description, help, execute visibility. |
| 163 | +4. Update `tests/CommandProviderTest.php`: change `assertCount(5, ...)` to `assertCount(6, ...)`, add `testContainsRebuildCacheInstance` checking `$commands[5]`. |
| 164 | +5. Run `vendor/bin/phpunit tests/Command/RebuildCacheTest.php && vendor/bin/phpunit tests/CommandProviderTest.php` — all green. |
| 165 | + |
| 166 | +**Result:** New `myadmin:rebuild-cache` command available via `composer myadmin:rebuild-cache`, fully tested. |
| 167 | + |
| 168 | +## Common Issues |
| 169 | + |
| 170 | +- **`CommandProviderTest::testGetCommandsReturnsFiveCommands` fails with "Failed asserting that 6 matches expected 5"**: You added the command to `CommandProvider.php` but forgot to update the count assertion in `tests/CommandProviderTest.php`. Change `assertCount(5, ...)` to `assertCount(6, ...)`. |
| 171 | + |
| 172 | +- **`Error: Class 'MyAdmin\Plugins\Command\NewCommand' not found`**: The class file exists but the namespace or class name doesn't match. Verify the file has `namespace MyAdmin\Plugins\Command;` and the class name matches the filename exactly (PascalCase). |
| 173 | + |
| 174 | +- **`The command "myadmin:foo" does not exist`**: The command was not registered in `src/CommandProvider.php`. Add both the `use` import and the `new ClassName()` entry in `getCommands()`. |
| 175 | + |
| 176 | +- **`Symfony\Component\Console\Exception\LogicException: The command name "" must not be empty`**: The `configure()` method is missing `->setName('myadmin:<name>')`. Ensure the `configure` method chains `setName()` first. |
| 177 | + |
| 178 | +- **Test instantiation fails with constructor errors**: Do NOT pass constructor arguments to command classes in tests. All existing commands are instantiated with `new ClassName()` (no arguments). The `BaseCommand` constructor is parameterless. |
0 commit comments