diff --git a/REFACTORING.md b/REFACTORING.md index c9eb3f7..9a11a06 100644 --- a/REFACTORING.md +++ b/REFACTORING.md @@ -39,6 +39,7 @@ ShiftPHP is moving toward an API-only modular monolith. View templates, compiled - [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] Developer documentation organized into a Laravel-like guide. - [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: diff --git a/docs/index.html b/docs/index.html index 91390c0..3ccdfec 100644 --- a/docs/index.html +++ b/docs/index.html @@ -3,94 +3,357 @@ - ShiftPHP Developer Documentation + ShiftPHP Documentation + -
-

ShiftPHP Developer Documentation

-

ShiftPHP is an API-only PHP 8.3 framework for modular monolith applications.

-
- - - -
-
-

Requirements

- -
- -
-

Architecture

-

The public framework namespace is Shift\. Composer maps it to the src/ directory.

-

Application code lives under application/. Modules live under application/modules/{ModuleName} and are autoloaded with the Modules\ namespace.

- -

Request Flow

-
Request
-  -> Shift\App
-  -> Middleware pipeline
-  -> Router
-  -> Controller action
-  -> Response
-  -> ResponseEmitter
+
+ + +
+
+

ShiftPHP Documentation

+

ShiftPHP is a small, zero-dependency, API-only framework. It is built around modules, attribute routing, typed request DTOs, a native PDO database layer, and a lightweight CLI.

+

This documentation describes the current framework codebase and the example Health module shipped in this repository.

+
+ +
+

Installation

+

ShiftPHP requires PHP 8.3 or newer, Composer, and the json and pdo PHP extensions.

+ +
composer install
+cp .env.example .env
+php -S 127.0.0.1:8000 index.php
+ +

Visit the example endpoint:

+ +
curl http://127.0.0.1:8000/health
+ +

Run the framework checks:

+ +
./shift doctor
+
+ +
+

Configuration

+

The bootstrap file loads .env from the project root. Existing server environment variables are not overwritten.

+ +
APP_ENV=local
+LOG_ENABLED=false
+LOG_PATH=storage/logs/shift.log
 
-            

Core Namespaces

-
-
Shift
-
Application kernel, request object, and base controller.
-
Shift\Response
-
Response objects and response emitter.
-
Shift\Routing
-
Attribute route loader and routing attributes.
-
Shift\Routing\Router
-
Router, route, and route match objects.
-
Shift\Middleware
-
Middleware contract and middleware pipeline.
-
Shift\Modules
-
Module contracts and module loader.
-
Shift\Service
-
Small service container and service interface.
-
Shift\Config
-
Environment variable loading and lookup.
-
Shift\Database
-
PDO database configuration, connection, and query helpers.
-
Shift\Console
-
CLI command dispatcher and built-in commands.
-
Shift\Error
-
HTTP and framework error handling.
-
-
- -
-

Bootstrap

-

The HTTP entry point is index.php. It creates a request, creates the app, loads modules, registers module services and routes, then starts the app.

- -
use Shift\App;
+DB_CONNECTION=mysql
+DB_HOST=127.0.0.1
+DB_PORT=3306
+DB_DATABASE=shift
+DB_USERNAME=root
+DB_PASSWORD=
+DB_CHARSET=utf8mb4
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
VariablePurpose
APP_ENVApplication environment label shown by diagnostics.
LOG_ENABLEDEnables JSON-line exception logs when set to true, 1, yes, or on.
LOG_PATHRelative or absolute path for the file logger.
DB_CONNECTIONDatabase driver. Supported values include mysql and sqlite.
+
+ +
+

Directory Structure

+

The framework lives in src/. Application code lives in application/, and modules live in application/modules.

+ +
.
+|-- application/
+|   `-- modules/
+|       `-- Health/
+|-- database/
+|   `-- migrations/
+|-- docs/
+|-- src/
+|-- storage/
+|   |-- cache/
+|   `-- logs/
+|-- tests/
+|-- index.php
+`-- shift
+
+ +
+

Request Lifecycle

+

The HTTP entry point creates a request, creates the app, loads modules, registers services and routes, then starts the app.

+ +
use Shift\App;
 use Shift\Modules\ModuleLoader;
 use Shift\Request;
 
@@ -102,209 +365,134 @@ 

Bootstrap

$modules = (new ModuleLoader())->load(); $modules->registerServices($app->getContainer()); $modules->registerRoutes($app->getRouter()); +$modules->boot($app->getContainer()); $app->start();
-

Run the application locally with PHP's built-in server:

-
php -S 127.0.0.1:8000 index.php
-
- -
-

Modules

-

A module owns its controllers, routes, services, config, lifecycle hooks, and CLI commands.

+

The internal flow is intentionally small:

-
application/modules/Health/
-|-- Module.php
-|-- Controllers/
-|-- Services/
-`-- Commands/
- -

Generator commands can also create Models/, Middleware/, and Dto/ directories when those artifacts are added.

+
Request
+  -> Shift\App
+  -> Middleware pipeline
+  -> Router
+  -> Controller action
+  -> Response
+  -> ResponseEmitter
+
-

Every module boundary implements Shift\Modules\ModuleInterface. Most modules can extend Shift\Modules\AbstractModule and override only the methods they need.

+
+

Routing

+

Routes are usually declared with PHP attributes on module-owned controllers and loaded with Shift\Routing\AttributeRouteLoader.

-
namespace Modules\Health;
+                
namespace Modules\Users\Controllers;
 
-use Shift\Modules\AbstractModule;
-use Shift\Routing\AttributeRouteLoader;
-use Shift\Routing\Router\Router;
-use Shift\Service\ServiceContainer;
-use Modules\Health\Controllers\HealthController;
-use Modules\Health\Services\HealthService;
+use Shift\Controller;
+use Shift\Routing\Attributes\Get;
+use Shift\Routing\Attributes\Post;
+use Shift\Routing\Attributes\RoutePrefix;
 
-class Module extends AbstractModule
+#[RoutePrefix('/users')]
+final class UserController extends Controller
 {
-    public function getName(): string
-    {
-        return 'health';
-    }
-
-    public function registerServices(ServiceContainer $container): void
-    {
-        $container->singleton(HealthService::class, HealthService::class);
-    }
-
-    public function registerRoutes(Router $router): void
-    {
-        (new AttributeRouteLoader())->load($router, [
-            HealthController::class,
-        ]);
-    }
-
-    public function getCommandMappings(): array
+    #[Get('/{id}')]
+    public function show(int $id): array
     {
-        return [
-            [
-                'dir' => __DIR__ . '/Commands/',
-                'namespace' => 'Modules\\Health\\Commands\\',
-            ],
-        ];
+        return ['id' => $id];
     }
 
-    public function boot(ServiceContainer $container): void
+    #[Post('')]
+    public function store(): array
     {
-        $container->singleton('health.booted', true);
+        return ['created' => true];
     }
 }
-

Shift\Modules\ModuleLoader discovers modules by convention from application/modules/*/Module.php. Module config can be returned from getConfig() or from a module-level config.php file. Merged config is available through $modules->getConfig() and the container singleton modules.config.

-

For production, discovered module metadata can be cached in storage/cache/modules.php with ./shift cache:modules. Clear it with ./shift cache:clear after changing module boundaries or module config.

-
- -
-

Routing

-

Routes are registered on Shift\Routing\Router\Router. Modules usually register routes through attributes and Shift\Routing\AttributeRouteLoader.

- -

Supported HTTP Method Attributes

-
    -
  • #[Get('/path')]
  • -
  • #[Post('/path')]
  • -
  • #[Put('/path')]
  • -
  • #[Patch('/path')]
  • -
  • #[Delete('/path')]
  • -
- -

Route Metadata Attributes

-
    -
  • #[RoutePrefix('/prefix')] on controller classes
  • -
  • #[Status(201)] on controller actions
  • -
  • #[Header('X-Name', 'value')] on controller actions
  • -
- -

Parameter Binding Attributes

-
    -
  • #[PathParam('id')] reads a route placeholder
  • -
  • #[QueryParam('include')] reads a query string value
  • -
  • #[Body] reads the decoded JSON body
  • -
  • #[Body('name')] reads one JSON body field
  • -
  • #[BodyDto] validates the JSON body into a request DTO
  • -
- -
#[RoutePrefix('/users')]
-final class UserController extends Controller
-{
-    #[Get('/{id}')]
-    public function show(
-        #[PathParam] int $id,
-        #[QueryParam('include')] ?string $include = null
-    ): JsonResponse {
-        return $this->json([
-            'id' => $id,
-            'include' => $include,
-        ]);
-    }
-}
+

Available Attributes

+ + + + + + + + + + + + + + + + + + +
AttributeUsed onDescription
#[RoutePrefix('/api')]ClassPrefixes all controller routes.
#[Get('/path')]MethodRegisters a GET route.
#[Post('/path')]MethodRegisters a POST route.
#[Put('/path')]MethodRegisters a PUT route.
#[Patch('/path')]MethodRegisters a PATCH route.
#[Delete('/path')]MethodRegisters a DELETE route.
#[Status(201)]MethodOverrides the response status.
#[Header('X-Name', 'value')]MethodAdds a response header.
+
+ +
+

Controllers

+

Controllers extend Shift\Controller. They are resolved through the service container, so typed constructor dependencies are autowired.

+ +
use Shift\Controller;
+use Shift\Response\JsonResponse;
+use Shift\Routing\Attributes\Get;
+use Shift\Routing\Attributes\PathParam;
 
-            

The router supports path placeholders such as /users/{id}. Unsupported HTTP methods return 405 Method Not Allowed with an Allow header.

-
- -
-

Controllers

-

Controllers extend Shift\Controller. Controllers are created through the service container, so typed constructor dependencies can be injected automatically.

- -

Controller actions can return:

-
    -
  • Shift\Response\Response
  • -
  • Shift\Response\JsonResponse
  • -
  • an array, which is normalized to JsonResponse
  • -
  • null, which is normalized to a 204 No Content response
  • -
  • a scalar value, which is normalized to a plain Response
  • -
- -

Controller Helpers

-
    -
  • $this->json(array $data, int $statusCode = 200)
  • -
  • $this->error(string $message, int $statusCode = 400, array $context = [], array $headers = [])
  • -
  • $this->noContent()
  • -
  • $this->getRequest()
  • -
  • $this->getContainer()
  • -
- -

Constructor Autowiring

-

Register services in a module, then type-hint them in a controller constructor.

- -
final class UserController extends Controller
+final class UserController extends Controller
 {
     public function __construct(private readonly UserService $users)
     {
     }
 
-    #[Get('/{id}')]
-    public function show(#[PathParam] int $id): array
+    #[Get('/users/{id}')]
+    public function show(#[PathParam] int $id): JsonResponse
     {
-        return $this->users->find($id);
+        return $this->json($this->users->find($id));
     }
 }
-

The app registers the current Shift\Request, Shift\Routing\Router\Router, and Shift\Service\ServiceContainer in the container by default.

-
+

Controller actions may return Response, JsonResponse, arrays, scalars, or null. Arrays are converted to JSON responses and null becomes a 204 No Content response.

+
-
-

Requests

-

Shift\Request wraps server data, query values, post data, route parameters, headers, and JSON request bodies.

+
+

Requests

+

Shift\Request wraps server data, query params, post data, headers, route params, attributes, and JSON request bodies.

-
$request->getMethod();
+                
$request->getMethod();
 $request->getPath();
-$request->getQueryParams();
-$request->getPostData();
 $request->query('page', 1);
 $request->post('name');
 $request->input('name');
-$request->getRawBody();
 $request->getJson();
 $request->getHeader('Authorization');
-$request->getUserAgent();
-$request->getIpAddress();
 $request->getRequestId();
-$request->getRouteParams();
 $request->routeParam('id');
-

If a 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.

-

getJson() returns an empty array for an empty body. Malformed JSON throws an HTTP error and is returned as 400 Bad Request.

-
- -
-

Responses

-

Shift\Response\Response stores response content, status code, and headers. Shift\Response\ResponseEmitter emits those values to PHP's HTTP response.

+

If the incoming request does not contain X-Request-Id, ShiftPHP generates one. Every response receives the same X-Request-Id header, and structured exception logs include it.

-
use Shift\Response\Response;
-use Shift\Response\JsonResponse;
+                
+ Malformed JSON +

Calling getJson() on malformed JSON throws an HTTP error and the app returns a 400 Bad Request JSON response.

+
+
-return new Response('Accepted', 202, ['X-State' => 'queued']); -return JsonResponse::ok(['status' => 'ok']); -return JsonResponse::created(['id' => 10]); -return JsonResponse::error('Invalid payload', 422); +
+

Responses

+

Use controller helpers for common API responses.

-

JsonResponse automatically sets Content-Type: application/json.

-
+
return $this->json(['status' => 'ok']);
+return $this->json($payload, 201);
+return $this->error('Invalid payload', 422);
+return $this->noContent();
-
-

Validation and DTOs

-

Shift\Validation\Validator validates arrays and returns validated values. Validation failures throw Shift\Validation\ValidationException, which the app emits as a 422 JSON response.

+

JsonResponse automatically sets Content-Type: application/json.

+
-

Supported rules are required, string, int, bool, array, email, min, and max.

+
+

Validation

+

Request DTOs extend Shift\Validation\RequestDto and declare rules with a static rules() method.

-
use Shift\Validation\RequestDto;
+                
use Shift\Validation\RequestDto;
 
 final class CreateUserDto extends RequestDto
 {
@@ -323,25 +511,20 @@ 

Validation and DTOs

} }
-

DTOs can be bound by type or with #[BodyDto]:

+

Bind DTOs explicitly with #[BodyDto], or type-hint a RequestDto subclass in an action.

-
#[Post('/users')]
-public function create(#[BodyDto] CreateUserDto $dto): array
+                
#[Post('/users')]
+public function store(#[BodyDto] CreateUserDto $dto): array
 {
-    return [
-        'email' => $dto->email,
-        'age' => $dto->age,
-    ];
+    return ['email' => $dto->email];
 }
-
- -
-

Middleware

-

Middleware runs before controller dispatch. It can continue the request by calling $next($request), modify the returned response, or return a response immediately.

+
-
namespace Modules\Users\Middleware;
+            
+

Middleware

+

Middleware may inspect, modify, or short-circuit a request before the controller action runs.

-use Shift\Middleware\MiddlewareInterface; +
use Shift\Middleware\MiddlewareInterface;
 use Shift\Request;
 use Shift\Response\JsonResponse;
 use Shift\Response\Response;
@@ -358,64 +541,114 @@ 

Middleware

} }
-

Register class middleware on the app:

-
$app->middleware(AuthMiddleware::class);
+

Register middleware on the app:

-

Callable middleware is also supported:

-
$app->middleware(function (Request $request, callable $next): Response {
-    $response = $next($request);
+                
$app->middleware(AuthMiddleware::class);
- return new Response( - $response->getContent(), - $response->getStatusCode(), - $response->getHeaders() + ['X-Api' => 'Shift'] - ); -});
+

Built-in middleware includes CORS, authentication, and authorization middleware.

+
-

Middleware may be a class string, an object implementing MiddlewareInterface, or a callable. Class strings are resolved from the service container when registered there.

+
+

Modules

+

Modules are the main application boundary. A module can own controllers, routes, services, commands, config, models, middleware, and DTOs.

-

Built-in Middleware

-
    -
  • Shift\Middleware\CorsMiddleware handles CORS headers and preflight requests.
  • -
  • Shift\Middleware\AuthMiddleware uses Shift\Auth\AuthenticatorInterface to authenticate a request.
  • -
  • Shift\Middleware\AuthorizationMiddleware uses Shift\Auth\AuthorizerInterface to authorize an authenticated user.
  • -
-
+
application/modules/Billing/
+|-- Module.php
+|-- Commands/
+|-- Controllers/
+|-- Services/
+|-- Models/
+|-- Middleware/
+`-- Dto/
-
-

Service Container

-

Shift\Service\ServiceContainer stores regular services and singletons. It can resolve closures, class names, and already-created objects. It can also build classes with typed constructor dependencies.

+

Create a module with the CLI:

-
$container->register(UserRepository::class, UserRepository::class);
-$container->singleton(HealthService::class, HealthService::class);
-$container->singleton('request', $request);
+                
./shift create:module Billing
-$service = $container->resolve(HealthService::class); -$controller = $container->make(HealthController::class); -$exists = $container->has(HealthService::class);
+

A module boundary usually extends Shift\Modules\AbstractModule.

-

The app registers the current request and router as default singleton services under request, Shift\Request, router, and Shift\Routing\Router\Router. It also registers the current Shift\Service\ServiceContainer instance, database config, and a lazy database service.

-
+
namespace Modules\Billing;
 
-        
-

Environment and Database

-

bootstrap.php loads .env from the project root without overwriting variables that already exist in the server environment.

+use Shift\Modules\AbstractModule; +use Shift\Routing\AttributeRouteLoader; +use Shift\Routing\Router\Router; +use Shift\Service\ServiceContainer; +use Modules\Billing\Controllers\InvoiceController; +use Modules\Billing\Services\InvoiceService; -
DB_CONNECTION=mysql
-DB_HOST=127.0.0.1
-DB_PORT=3306
-DB_DATABASE=shift
-DB_USERNAME=root
-DB_PASSWORD=
-DB_CHARSET=utf8mb4
-LOG_ENABLED=false
-LOG_PATH=storage/logs/shift.log
+final class Module extends AbstractModule +{ + public function getName(): string + { + return 'billing'; + } + + public function registerServices(ServiceContainer $container): void + { + $container->singleton(InvoiceService::class, InvoiceService::class); + } + + public function registerRoutes(Router $router): void + { + (new AttributeRouteLoader())->load($router, [ + InvoiceController::class, + ]); + } +}
+
+ +
+

Service Container

+

The service container stores regular services and singletons. It can resolve closures, class names, objects, and typed constructor dependencies.

+ +
$container->register(UserRepository::class, UserRepository::class);
+$container->singleton(UserService::class, UserService::class);
+
+$service = $container->resolve(UserService::class);
+$controller = $container->make(UserController::class);
+
+ +
+

Console Commands

+

Commands implement Shift\Console\CommandInterface and may declare metadata with #[Command].

+ +
use Shift\Console\Attributes\Command;
+use Shift\Console\Cli;
+use Shift\Console\CommandInterface;
+
+#[Command('billing:sync', aliases: ['sync-billing'], group: 'modules')]
+final class SyncBilling implements CommandInterface
+{
+    public function execute(mixed ...$args): void
+    {
+        (new Cli())->success('Billing synced.');
+    }
+
+    public function getHelp(): string
+    {
+        return 'Usage: ./shift billing:sync';
+    }
+
+    public function getDescription(): string
+    {
+        return 'Sync billing data.';
+    }
+}
-

Database access uses native PDO. The app registers Shift\Database\DatabaseConfig, Shift\Database\Database, and the db alias lazily in the container.

+

The help command groups commands by metadata and resolves aliases.

-
use Shift\Database\Database;
+                
./shift help
+./shift help billing:sync
+./shift sync-billing
+
-class UserService +
+

Database

+

ShiftPHP uses native PDO and registers Shift\Database\Database and the db alias lazily in the service container.

+ +
use Shift\Database\Database;
+
+final class UserService
 {
     public function __construct(private readonly Database $db)
     {
@@ -429,25 +662,31 @@ 

Environment and Database

} }
-

Use query() for prepared queries, execute() for write statements, pdo() for raw PDO access, and transaction() for transactional work.

+

The core helpers are query(), execute(), table(), pdo(), transaction(), and lastInsertId().

+
-

Query Builder

-
$users = $db->table('users')
+            
+

Query Builder

+

The table query builder supports simple fluent selects, inserts, updates, deletes, ordering, limits, and offsets.

+ +
$users = $db->table('users')
     ->select('id', 'email')
     ->where('active', true)
     ->orderBy('id', 'desc')
     ->limit(10)
     ->get();
+
-

Models

-

Models extend Shift\Database\Model. Public properties are database columns, and model attributes describe primary keys, guarded fields, and casts.

+
+

Models

+

Models extend Shift\Database\Model. Public properties represent columns. Attributes define primary keys, guarded fields, and casts.

-
use Shift\Database\Attributes\Cast;
+                
use Shift\Database\Attributes\Cast;
 use Shift\Database\Attributes\Guarded;
 use Shift\Database\Attributes\PrimaryKey;
 use Shift\Database\Model;
 
-class User extends Model
+final class User extends Model
 {
     protected string $table = 'users';
 
@@ -462,43 +701,23 @@ 

Models

#[Cast('array')] public array $meta = []; - - #[Cast('datetime')] - public ?DateTimeImmutable $created_at = null; }
-
$user = User::query($db)->where('email', 'dev@example.com')->first();
-$user = User::find(1, $db);
+                
$user = User::find(1, $db);
 $user = User::create(['email' => 'dev@example.com'], $db);
 $user->role = 'admin';
 $user->save($db);
+
-

#[Guarded] fields are ignored during mass assignment through create() and query update(), but can be set explicitly before save(). Supported casts include int, float, bool, string, array, date, datetime, and class names. Class casts use fromArray() when available.

- - -
-

Logging

-

Structured exception logging is available through Shift\Logging\LoggerInterface. Logging is disabled by default and can be enabled with environment variables.

- -
LOG_ENABLED=true
-LOG_PATH=storage/logs/shift.log
+
+

Migrations

+

Create migration files in database/migrations:

-

The file logger writes JSON lines with timestamp, level, message, and context. Exception context includes exception class, status code, file, line, and request data such as method, path, IP, user agent, and X-Request-Id when present.

+
./shift create:migration create_users_table
-
use Shift\Logging\LoggerInterface;
+                

A migration returns an anonymous class extending Shift\Database\Migration.

-$app->getContainer()->singleton(LoggerInterface::class, new CustomLogger());
-
- -
-

Migrations

-

Migration files live in database/migrations. Create a migration with the CLI:

- -
./shift create:migration create_users_table
- -

A migration returns an anonymous class extending Shift\Database\Migration.

- -
use Shift\Database\Database;
+                
use Shift\Database\Database;
 use Shift\Database\Migration;
 
 return new class extends Migration
@@ -514,102 +733,65 @@ 

Migrations

} };
-

The migration runner stores applied migrations in the migrations table and runs each migration inside a database transaction.

- -
./shift migrate
+                
./shift migrate
 ./shift migrate:status
 ./shift migrate:rollback
-
+
-
-

CLI

-

The CLI entry point is shift. Built-in commands live under Console\Commands, and module commands are loaded from module command mappings.

-

Shift\Console\CommandRegistry discovers framework, application, and module commands. Commands can declare their name, aliases, and help group with #[Command]. It normalizes command names, so migrate:status, migrate-status, and migrate_status resolve to the same command.

+
+

Logging

+

Structured exception logging is available through Shift\Logging\LoggerInterface. Logging is disabled by default and can be enabled with environment variables.

-
use Shift\Console\Attributes\Command;
-use Shift\Console\CommandInterface;
+                
LOG_ENABLED=true
+LOG_PATH=storage/logs/shift.log
-#[Command('billing:sync', aliases: ['sync-billing'], group: 'modules')] -final class SyncBilling implements CommandInterface -{ - public function execute(mixed ...$args): void - { - } +

The file logger writes JSON lines with timestamp, level, message, and context. Exception context includes class, status, code, file, line, and request metadata.

- public function getHelp(): string - { - return 'Usage: ./shift billing:sync'; - } +
use Shift\Logging\LoggerInterface;
 
-    public function getDescription(): string
-    {
-        return 'Sync billing data.';
-    }
-}
+$app->getContainer()->singleton(LoggerInterface::class, new CustomLogger());
+
+ +
+

Cache

+

Module discovery can be cached for production. The generated cache file lives at storage/cache/modules.php.

-
./shift help
-./shift help migrate
-./shift help ms
-./shift doctor
-./shift route:list
-./shift test
-./shift health
-./shift about
-./shift env:check
-./shift db:check
-./shift module:list
-./shift cache:modules
+                
./shift cache:modules
 ./shift cache:status
 ./shift cache:clear
-

Create commands scaffold modules and module-owned classes:

+

Rebuild the module cache after changing module boundaries, module config, or module command mappings.

+
-
./shift create:module Billing
-./shift create:controller --module=Billing InvoiceController
-./shift create:controller Billing:InvoiceController
-./shift create:model Billing:Invoice
-./shift create:service Billing:Invoice
-./shift create:command Billing:SyncInvoices
-./shift create:middleware Billing:Audit
-./shift create:dto Billing:CreateInvoice
-./shift create:migration create_users_table
+
+

Doctor

+

The doctor command runs local diagnostics and returns a non-zero exit code when a required check fails.

-

Migration commands manage database schema changes:

+
./shift doctor
-
./shift migrate
-./shift migrate:status
-./shift migrate:rollback
- -

Controller, service, middleware, and DTO generators add the expected class suffix when it is missing. Commands accept either --module=Billing Name or Billing:Name.

-

Generator templates live in src/Console/Generator/stubs.

- -

Commands implement Shift\Console\CommandInterface.

-
- -
-

Errors

-

Framework HTTP errors are represented by Shift\Error\HttpError. The app normalizes HTTP errors to JSON responses.

- -
{
-  "error": {
-    "message": "Endpoint not found",
-    "status": 404
-  }
-}
+

It checks PHP version, required extensions, Composer JSON validity, PHP lint, the test suite, environment presence, database config, and module cache status.

+
-

Malformed JSON returns 400. Missing routes return 404. Wrong methods return 405 with an Allow header. Unexpected runtime errors return a generic 500 Internal Server Error.

-
+
+

Testing

+

The project has a lightweight test runner in tests/ApiCoreTest.php. Shared helpers live in tests/Support, fixtures in tests/Fixtures, and feature tests in tests/Feature.

-
-

Testing

-

The current lightweight test runner is tests/ApiCoreTest.php.

+
composer test
+./shift test
-
composer test
+

The GitHub workflow validates Composer config, dumps autoload files, lints PHP files, runs tests, and verifies the route list command.

+
-

Shared assertions, request helpers, and emitters live in tests/Support. Test-only controllers, DTOs, middleware, and auth fixtures live in tests/Fixtures. Feature test files live in tests/Feature.

+
+

Releases

+

Each pull request must have exactly one version label, such as v0.20.0. After a versioned PR is merged into master, GitHub Actions creates the matching tag and GitHub Release.

-

The API workflow also validates Composer configuration, dumps autoload files, lints PHP files, runs the API tests, and verifies the route list command.

-
-
+
release/v0.20.0
+label: v0.20.0
+merge into master
+automatic tag and release
+ + +