Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,10 +147,11 @@ $request->post('name');
$request->input('name');
$request->getJson();
$request->getHeader('Authorization');
$request->getRequestId();
$request->routeParam('id');
```

Malformed JSON bodies are returned as `400 Bad Request`.
If the request does not include `X-Request-Id`, ShiftPHP generates one. The same id is emitted on every response as `X-Request-Id` and included in structured exception logs. Malformed JSON bodies are returned as `400 Bad Request`.

## Validation and DTOs

Expand Down Expand Up @@ -405,13 +406,37 @@ http://127.0.0.1:8000/health

## CLI

The CLI discovers framework, application, and module commands through the command registry. Command names are normalized, so `migrate:status`, `migrate-status`, and `migrate_status` resolve to the same command.
The CLI discovers framework, application, and module commands through the command registry. Command names are declared with `#[Command]` attributes and can include aliases and groups. Command names are normalized, so `migrate:status`, `migrate-status`, and `migrate_status` resolve to the same command.

```php
use Shift\Console\Attributes\Command;
use Shift\Console\CommandInterface;

#[Command('billing:sync', aliases: ['sync-billing'], group: 'modules')]
final class SyncBilling implements CommandInterface
{
public function execute(mixed ...$args): void
{
}

public function getHelp(): string
{
return 'Usage: ./shift billing:sync';
}

public function getDescription(): string
{
return 'Sync billing data.';
}
}
```

Show all commands or command-specific help:

```sh
./shift help
./shift help migrate
./shift help ms
```

List registered API routes:
Expand Down Expand Up @@ -441,6 +466,7 @@ Inspect framework/runtime information:
Check local environment and database configuration:

```sh
./shift doctor
./shift env:check
./shift db:check
```
Expand Down
6 changes: 4 additions & 2 deletions REFACTORING.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,12 @@ ShiftPHP is moving toward an API-only modular monolith. View templates, compiled
- [x] CLI diagnostics for tests, runtime info, environment, database, and modules.
- [x] CLI help listing and command-specific usage.
- [x] Centralized CLI command registry shared by the dispatcher and help command.
- [x] CLI command metadata through attributes, aliases, and grouped help.
- [x] `shift doctor` project diagnostics.
- [x] Database migrations with create, migrate, status, and rollback commands.
- [x] Module discovery cache for production.
- [x] Structured exception logging with JSON file logger and service container override.
- [x] Request id lifecycle with generated `X-Request-Id` response headers and log context.
- [x] Removal of view storage and example page assets from runtime.
- [x] Removal of legacy `application/controllers` and `application/routes.php`.
- [x] Domain-oriented framework namespaces:
Expand Down Expand Up @@ -161,5 +164,4 @@ Internal errors return a generic `500` message unless `display_errors` is enable

## Next

- [ ] CLI command aliases and richer command metadata.
- [ ] Basic package-quality checks, for example static analysis and coding style.
- [ ] Static analysis and coding style checks.
1 change: 1 addition & 0 deletions application/modules/Health/Commands/Health.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Shift\Console\CommandInterface;
use Modules\Health\Services\HealthService;

#[\Shift\Console\Attributes\Command('health', group: 'modules')]
class Health implements CommandInterface
{
public function execute(mixed ...$args): void
Expand Down
27 changes: 26 additions & 1 deletion docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -275,9 +275,11 @@ <h2>Requests</h2>
$request-&gt;getHeader('Authorization');
$request-&gt;getUserAgent();
$request-&gt;getIpAddress();
$request-&gt;getRequestId();
$request-&gt;getRouteParams();
$request-&gt;routeParam('id');</code></pre>

<p>If a request does not include <code>X-Request-Id</code>, ShiftPHP generates one. The same id is emitted on every response as <code>X-Request-Id</code> and included in structured exception logs.</p>
<p><code>getJson()</code> returns an empty array for an empty body. Malformed JSON throws an HTTP error and is returned as <code>400 Bad Request</code>.</p>
</section>

Expand Down Expand Up @@ -522,10 +524,33 @@ <h2>Migrations</h2>
<section id="cli">
<h2>CLI</h2>
<p>The CLI entry point is <code>shift</code>. Built-in commands live under <code>Console\Commands</code>, and module commands are loaded from module command mappings.</p>
<p><code>Shift\Console\CommandRegistry</code> discovers framework, application, and module commands. It normalizes command names, so <code>migrate:status</code>, <code>migrate-status</code>, and <code>migrate_status</code> resolve to the same command.</p>
<p><code>Shift\Console\CommandRegistry</code> discovers framework, application, and module commands. Commands can declare their name, aliases, and help group with <code>#[Command]</code>. It normalizes command names, so <code>migrate:status</code>, <code>migrate-status</code>, and <code>migrate_status</code> resolve to the same command.</p>

<pre><code>use Shift\Console\Attributes\Command;
use Shift\Console\CommandInterface;

#[Command('billing:sync', aliases: ['sync-billing'], group: 'modules')]
final class SyncBilling implements CommandInterface
{
public function execute(mixed ...$args): void
{
}

public function getHelp(): string
{
return 'Usage: ./shift billing:sync';
}

public function getDescription(): string
{
return 'Sync billing data.';
}
}</code></pre>

<pre><code>./shift help
./shift help migrate
./shift help ms
./shift doctor
./shift route:list
./shift test
./shift health
Expand Down
11 changes: 10 additions & 1 deletion src/App.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ public function start(): void
$response = JsonResponse::error('Internal Server Error', 500);
}

$this->emitter->emit($response);
$this->emitter->emit($this->withRequestId($response));
}

public function middleware(MiddlewareInterface|callable|string $middleware): self
Expand Down Expand Up @@ -398,4 +398,13 @@ private function logException(Throwable $exception, int $statusCode): void
} catch (Throwable) {
}
}

private function withRequestId(Response $response): Response
{
return new Response(
$response->getContent(),
$response->getStatusCode(),
['X-Request-Id' => $this->request->getRequestId()] + $response->getHeaders()
);
}
}
19 changes: 19 additions & 0 deletions src/Console/Attributes/Command.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace Shift\Console\Attributes;

use Attribute;

#[Attribute(Attribute::TARGET_CLASS)]
final class Command
{
/**
* @param list<string> $aliases
*/
public function __construct(
public readonly string $name,
public readonly array $aliases = [],
public readonly string $group = 'general'
) {
}
}
23 changes: 23 additions & 0 deletions src/Console/CommandDefinition.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace Shift\Console;

final class CommandDefinition
{
/**
* @param class-string<CommandInterface> $class
* @param list<string> $aliases
*/
public function __construct(
public readonly string $name,
public readonly string $class,
public readonly array $aliases = [],
public readonly string $group = 'general'
) {
}

public function instantiate(): CommandInterface
{
return new $this->class();
}
}
72 changes: 67 additions & 5 deletions src/Console/CommandRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace Shift\Console;

use ReflectionClass;
use Shift\Console\Attributes\Command;
use Shift\Modules\ModuleLoader;

final class CommandRegistry
Expand All @@ -23,7 +25,18 @@ public static function default(): self
*/
public function all(): array
{
$commands = [];
return array_map(
static fn (CommandDefinition $definition): string => $definition->class,
$this->definitions()
);
}

/**
* @return array<string, CommandDefinition>
*/
public function definitions(): array
{
$definitions = [];

foreach ($this->mappings() as $mapping) {
if (!is_dir($mapping['dir'])) {
Expand All @@ -37,22 +50,36 @@ public function all(): array
$class = $mapping['namespace'] . $className;

if (class_exists($class) && is_subclass_of($class, CommandInterface::class)) {
$commands[self::nameFromClass($className)] = $class;
$definition = $this->definitionFor($class, $className);
$definitions[$definition->name] = $definition;
}
}
}

ksort($commands);
ksort($definitions);

return $commands;
return $definitions;
}

/**
* @return class-string<CommandInterface>|null
*/
public function find(string $command): ?string
{
return $this->all()[$this->normalize($command)] ?? null;
return $this->findDefinition($command)?->class;
}

public function findDefinition(string $command): ?CommandDefinition
{
$normalized = $this->normalize($command);

foreach ($this->definitions() as $definition) {
if ($definition->name === $normalized || in_array($normalized, $definition->aliases, true)) {
return $definition;
}
}

return null;
}

public function normalize(string $command): string
Expand Down Expand Up @@ -83,6 +110,41 @@ public static function nameFromClass(string $class): string
return implode(':', $commandParts);
}

/**
* @param class-string<CommandInterface> $class
*/
private function definitionFor(string $class, string $fallbackClassName): CommandDefinition
{
$reflection = new ReflectionClass($class);
$attributes = $reflection->getAttributes(Command::class);

if ($attributes !== []) {
/** @var Command $attribute */
$attribute = $attributes[0]->newInstance();

return new CommandDefinition(
$this->normalize($attribute->name),
$class,
$this->normalizeList($attribute->aliases),
$attribute->group
);
}

return new CommandDefinition(self::nameFromClass($fallbackClassName), $class);
}

/**
* @param list<string> $values
* @return list<string>
*/
private function normalizeList(array $values): array
{
return array_values(array_filter(array_map(
fn (string $value): string => $this->normalize($value),
$values
)));
}

/**
* @return list<array{dir: string, namespace: string}>
*/
Expand Down
1 change: 1 addition & 0 deletions src/Console/Commands/About.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Shift\Console\Cli;
use Shift\Console\CommandInterface;

#[\Shift\Console\Attributes\Command('about', group: 'diagnostics')]
class About implements CommandInterface
{
public function execute(mixed ...$args): void
Expand Down
1 change: 1 addition & 0 deletions src/Console/Commands/CacheClear.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Shift\Console\CommandInterface;
use Shift\Modules\ModuleLoader;

#[\Shift\Console\Attributes\Command('cache:clear', aliases: ['cc'], group: 'cache')]
class CacheClear implements CommandInterface
{
public function execute(mixed ...$args): void
Expand Down
1 change: 1 addition & 0 deletions src/Console/Commands/CacheModules.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Shift\Console\CommandInterface;
use Shift\Modules\ModuleLoader;

#[\Shift\Console\Attributes\Command('cache:modules', aliases: ['cm'], group: 'cache')]
class CacheModules implements CommandInterface
{
public function execute(mixed ...$args): void
Expand Down
1 change: 1 addition & 0 deletions src/Console/Commands/CacheStatus.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Shift\Console\CommandInterface;
use Shift\Modules\ModuleLoader;

#[\Shift\Console\Attributes\Command('cache:status', aliases: ['cs'], group: 'cache')]
class CacheStatus implements CommandInterface
{
public function execute(mixed ...$args): void
Expand Down
1 change: 1 addition & 0 deletions src/Console/Commands/CreateCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Shift\Console\Generator\GeneratesFiles;
use Shift\Console\Generator\NameFormatter;

#[\Shift\Console\Attributes\Command('create:command', group: 'make')]
class CreateCommand implements CommandInterface
{
use GeneratesFiles;
Expand Down
1 change: 1 addition & 0 deletions src/Console/Commands/CreateController.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Shift\Console\Generator\GeneratesFiles;
use Shift\Console\Generator\NameFormatter;

#[\Shift\Console\Attributes\Command('create:controller', group: 'make')]
class CreateController implements CommandInterface
{
use GeneratesFiles;
Expand Down
1 change: 1 addition & 0 deletions src/Console/Commands/CreateDto.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Shift\Console\Generator\GeneratesFiles;
use Shift\Console\Generator\NameFormatter;

#[\Shift\Console\Attributes\Command('create:dto', group: 'make')]
class CreateDto implements CommandInterface
{
use GeneratesFiles;
Expand Down
1 change: 1 addition & 0 deletions src/Console/Commands/CreateMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Shift\Console\Generator\GeneratesFiles;
use Shift\Console\Generator\NameFormatter;

#[\Shift\Console\Attributes\Command('create:middleware', group: 'make')]
class CreateMiddleware implements CommandInterface
{
use GeneratesFiles;
Expand Down
1 change: 1 addition & 0 deletions src/Console/Commands/CreateMigration.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Shift\Console\Generator\NameFormatter;
use Shift\Console\Generator\StubRenderer;

#[\Shift\Console\Attributes\Command('create:migration', group: 'database')]
class CreateMigration implements CommandInterface
{
public function execute(mixed ...$args): void
Expand Down
1 change: 1 addition & 0 deletions src/Console/Commands/CreateModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Shift\Console\Generator\GeneratesFiles;
use Shift\Console\Generator\NameFormatter;

#[\Shift\Console\Attributes\Command('create:model', group: 'make')]
class CreateModel implements CommandInterface
{
use GeneratesFiles;
Expand Down
Loading
Loading