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
52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,42 @@ class UserService

Available helpers are `query($sql, $parameters)`, `execute($sql, $parameters)`, `pdo()`, and `transaction($callback)`. Query results expose `all()`, `first()`, `value()`, `affectedRows()`, and the raw `PDOStatement`.

### Migrations

Create migration files under `database/migrations`:

```sh
./shift create:migration create_users_table
```

Migration files return an anonymous `Shift\Database\Migration` instance:

```php
use Shift\Database\Database;
use Shift\Database\Migration;

return new class extends Migration
{
public function up(Database $db): void
{
$db->execute('create table users (id integer primary key autoincrement, email text not null)');
}

public function down(Database $db): void
{
$db->execute('drop table users');
}
};
```

Run pending migrations, inspect their status, or roll back the latest batch:

```sh
./shift migrate
./shift migrate:status
./shift migrate:rollback
```

### Query Builder and Models

Use the table query builder for simple fluent queries:
Expand Down Expand Up @@ -347,6 +383,13 @@ http://127.0.0.1:8000/health

## CLI

Show all commands or command-specific help:

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

List registered API routes:

```sh
Expand Down Expand Up @@ -384,6 +427,15 @@ List discovered modules:
./shift module:list
```

Run database migrations:

```sh
./shift create:migration create_users_table
./shift migrate
./shift migrate:status
./shift migrate:rollback
```

Generate module scaffolding:

```sh
Expand Down
4 changes: 3 additions & 1 deletion REFACTORING.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ ShiftPHP is moving toward an API-only modular monolith. View templates, compiled
- [x] Native PDO database configuration, lazy connection, and basic query API.
- [x] Fluent query builder and attribute-driven database models.
- [x] CLI diagnostics for tests, runtime info, environment, database, and modules.
- [x] CLI help listing and command-specific usage.
- [x] Database migrations with create, migrate, status, and rollback commands.
- [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 @@ -158,5 +160,5 @@ Internal errors return a generic `500` message unless `display_errors` is enable

- [ ] Structured logging for exceptions.
- [ ] Module discovery cache for production.
- [ ] CLI command namespaces and command metadata.
- [ ] CLI command aliases and richer command metadata.
- [ ] Basic package-quality checks, for example static analysis and coding style.
1 change: 1 addition & 0 deletions database/migrations/.gitkeep
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

55 changes: 48 additions & 7 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,12 @@ <h2>Contents</h2>
<li><a href="#responses">Responses</a></li>
<li><a href="#validation">Validation and DTOs</a></li>
<li><a href="#middleware">Middleware</a></li>
<li><a href="#services">Service Container</a></li>
<li><a href="#database">Environment and Database</a></li>
<li><a href="#cli">CLI</a></li>
<li><a href="#errors">Errors</a></li>
<li><a href="#testing">Testing</a></li>
<li><a href="#services">Service Container</a></li>
<li><a href="#database">Environment and Database</a></li>
<li><a href="#migrations">Migrations</a></li>
<li><a href="#cli">CLI</a></li>
<li><a href="#errors">Errors</a></li>
<li><a href="#testing">Testing</a></li>
</ol>
</nav>

Expand Down Expand Up @@ -469,11 +470,44 @@ <h3>Models</h3>
<p><code>#[Guarded]</code> fields are ignored during mass assignment through <code>create()</code> and query <code>update()</code>, but can be set explicitly before <code>save()</code>. Supported casts include <code>int</code>, <code>float</code>, <code>bool</code>, <code>string</code>, <code>array</code>, <code>date</code>, <code>datetime</code>, and class names. Class casts use <code>fromArray()</code> when available.</p>
</section>

<section id="migrations">
<h2>Migrations</h2>
<p>Migration files live in <code>database/migrations</code>. Create a migration with the CLI:</p>

<pre><code>./shift create:migration create_users_table</code></pre>

<p>A migration returns an anonymous class extending <code>Shift\Database\Migration</code>.</p>

<pre><code>use Shift\Database\Database;
use Shift\Database\Migration;

return new class extends Migration
{
public function up(Database $db): void
{
$db-&gt;execute('create table users (id integer primary key autoincrement, email text not null)');
}

public function down(Database $db): void
{
$db-&gt;execute('drop table users');
}
};</code></pre>

<p>The migration runner stores applied migrations in the <code>migrations</code> table and runs each migration inside a database transaction.</p>

<pre><code>./shift migrate
./shift migrate:status
./shift migrate:rollback</code></pre>
</section>

<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>

<pre><code>./shift route:list
<pre><code>./shift help
./shift help migrate
./shift route:list
./shift test
./shift health
./shift about
Expand All @@ -490,7 +524,14 @@ <h2>CLI</h2>
./shift create:service Billing:Invoice
./shift create:command Billing:SyncInvoices
./shift create:middleware Billing:Audit
./shift create:dto Billing:CreateInvoice</code></pre>
./shift create:dto Billing:CreateInvoice
./shift create:migration create_users_table</code></pre>

<p>Migration commands manage database schema changes:</p>

<pre><code>./shift migrate
./shift migrate:status
./shift migrate:rollback</code></pre>

<p>Controller, service, middleware, and DTO generators add the expected class suffix when it is missing. Commands accept either <code>--module=Billing Name</code> or <code>Billing:Name</code>.</p>
<p>Generator templates live in <code>src/Console/Generator/stubs</code>.</p>
Expand Down
44 changes: 44 additions & 0 deletions src/Console/Commands/CreateMigration.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

namespace Console\Commands;

use Shift\Console\Cli;
use Shift\Console\CommandInterface;
use Shift\Console\Generator\FileGenerator;
use Shift\Console\Generator\NameFormatter;
use Shift\Console\Generator\StubRenderer;

class CreateMigration implements CommandInterface
{
public function execute(mixed ...$args): void
{
$cli = new Cli();
$name = $args[0] ?? null;

if (!is_string($name) || $name === '') {
$cli->error($this->getHelp());
return;
}

$directory = APP_ROOT . '/database/migrations';
$migrationName = str_replace('-', '_', NameFormatter::slug($name));
$file = $directory . '/' . date('Y_m_d_His') . '_' . $migrationName . '.php';
$content = (new StubRenderer())->render('migration', [
'name' => $migrationName,
]);

$files = new FileGenerator();
$files->writeFile($file, $content);
$cli->success('Created: ' . $file);
}

public function getHelp(): string
{
return 'Usage: ./shift create:migration {name}';
}

public function getDescription(): string
{
return 'Create a database migration file.';
}
}
141 changes: 105 additions & 36 deletions src/Console/Commands/Help.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,70 +8,139 @@

namespace Console\Commands;

use Shift\Console\Cli;
use Shift\Console\CommandInterface;
use Shift\Modules\ModuleLoader;

class Help implements CommandInterface
{
/** @var array<array{dir: string, namespace: string}> */
private array $mappings = [
[
'dir' => APP_PATH . '/console/',
'namespace' => 'AppConsole\\Commands\\'
],
[
'dir' => APP_ROOT . '/src/Console/Commands/',
'namespace' => 'Console\\Commands\\'
],
];

/**
* @param mixed ...$args
*/
public function execute(mixed ...$args): void
{
$commandName = $args[0] ?? '';
$commandName = $args[0] ?? null;

if ($commandName) {
if (is_string($commandName) && $commandName !== '') {
$this->displayHelpForCommand($commandName);
} else {
$this->displayFullHelp();
return;
}

$this->displayFullHelp();
}

/**
* @param string $command
*/
private function displayHelpForCommand(string $command): void
{
$found = false;
$cli = new Cli();
$class = $this->findCommandClass($this->normalizeCommandName($command));

foreach ($this->mappings as $mapping) {
if (!$found && file_exists($mapping['dir'] . $command . '.php')) {
require_once($mapping['dir'] . $command . '.php');
$found = $mapping['namespace'] . $command;
}
if ($class === null) {
$cli->error('Command not found: ' . $command);
return;
}

$instance = new $class();
$cli->info($this->classToCommand($this->shortClass($class)));
$cli->debug($instance->getDescription());
$cli->debug($instance->getHelp());
}

private function displayFullHelp(): void
{
$cli = new Cli();
$rows = [];

foreach ($this->commandClasses() as $className => $class) {
$instance = new $class();
$rows[] = [
$this->classToCommand($className),
$instance->getDescription(),
];
}

usort($rows, static fn (array $left, array $right): int => strcmp($left[0], $right[0]));

$cli->table(['Command', 'Description'], $rows);
}

public function getHelp(): string
{
return 'Usage: ./shift help [command]';
}

public function getDescription(): string
{
return 'Show available commands.';
}

private function findCommandClass(string $className): ?string
{
return $this->commandClasses()[$className] ?? null;
}

/**
* @return string
* @return array<string, class-string<CommandInterface>>
*/
public function getHelp(): string
private function commandClasses(): array
{
// TODO: Implement getHelp() method.
return '';
$classes = [];

foreach ($this->mappings() as $mapping) {
if (!is_dir($mapping['dir'])) {
continue;
}

foreach (glob($mapping['dir'] . '*.php') ?: [] as $file) {
$className = pathinfo($file, PATHINFO_FILENAME);
require_once $file;
$class = $mapping['namespace'] . $className;

if (class_exists($class) && is_subclass_of($class, CommandInterface::class)) {
$classes[$className] = $class;
}
}
}

return $classes;
}

/**
* @return string
* @return list<array{dir: string, namespace: string}>
*/
public function getDescription(): string
private function mappings(): array
{
return array_merge(
[
[
'dir' => APP_PATH . '/console/',
'namespace' => 'AppConsole\\Commands\\',
],
[
'dir' => APP_ROOT . '/src/Console/Commands/',
'namespace' => 'Console\\Commands\\',
],
],
(new ModuleLoader())->load()->getCommandMappings()
);
}

private function normalizeCommandName(string $command): string
{
$parts = preg_split('/[:\-_]/', $command) ?: [];
$parts = array_map(static fn (string $part): string => ucfirst($part), $parts);

return implode('', $parts);
}

private function classToCommand(string $class): string
{
// TODO: Implement getDescription() method.
return '';
$parts = preg_split('/(?=[A-Z])/', $class, -1, PREG_SPLIT_NO_EMPTY) ?: [];
$parts = array_map(static fn (string $part): string => strtolower($part), $parts);

return implode(':', $parts);
}

private function shortClass(string $class): string
{
$parts = explode('\\', $class);

return end($parts) ?: $class;
}
}
Loading
Loading