Requirements
+-
+
- PHP 8.3 or newer +
- Composer +
- The
jsonPHP extension
+
Architecture
+The public framework namespace is Shift\. Composer currently maps it to the Engine/ 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
+
+ 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\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;
+use Shift\Modules\ModuleLoader;
+use Shift\Request;
+
+require_once 'bootstrap.php';
+
+$request = new Request();
+$app = new App($request);
+
+$modules = (new ModuleLoader())->load();
+$modules->registerServices($app->getContainer());
+$modules->registerRoutes($app->getRouter());
+
+$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, and CLI commands.
+ +application/modules/Health/
+|-- Module.php
+|-- Controllers/
+|-- Services/
+`-- Commands/
+
+ Every module boundary implements Shift\Modules\ModuleInterface. Most modules can extend Shift\Modules\AbstractModule and override only the methods they need.
namespace Modules\Health;
+
+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;
+
+class Module extends AbstractModule
+{
+ 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
+ {
+ return [
+ [
+ 'dir' => __DIR__ . '/Commands/',
+ 'namespace' => 'Modules\\Health\\Commands\\',
+ ],
+ ];
+ }
+}
+
+ Shift\Modules\ModuleLoader discovers modules by convention from application/modules/*/Module.php.
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
+
#[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,
+ ]);
+ }
+}
+
+ 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. The current Shift\Request and Shift\Service\ServiceContainer are injected through the constructor.
Controller actions can return:
+-
+
Shift\Response\Response
+ Shift\Response\JsonResponse
+ - an array, which is normalized to
JsonResponse
+ null, which is normalized to a204 No Contentresponse
+ - 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()
+
Requests
+Shift\Request wraps server data, query values, post data, route parameters, headers, and JSON request bodies.
$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->getRouteParams();
+$request->routeParam('id');
+
+ 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.
use Shift\Response\Response;
+use Shift\Response\JsonResponse;
+
+return new Response('Accepted', 202, ['X-State' => 'queued']);
+return JsonResponse::ok(['status' => 'ok']);
+return JsonResponse::created(['id' => 10]);
+return JsonResponse::error('Invalid payload', 422);
+
+ JsonResponse automatically sets Content-Type: application/json.
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;
+
+use Shift\Middleware\MiddlewareInterface;
+use Shift\Request;
+use Shift\Response\JsonResponse;
+use Shift\Response\Response;
+
+final class AuthMiddleware implements MiddlewareInterface
+{
+ public function handle(Request $request, callable $next): Response
+ {
+ if ($request->getHeader('Authorization') === null) {
+ return JsonResponse::error('Unauthorized', 401);
+ }
+
+ return $next($request);
+ }
+}
+
+ Register class middleware on the app:
+$app->middleware(AuthMiddleware::class);
+
+ Callable middleware is also supported:
+$app->middleware(function (Request $request, callable $next): Response {
+ $response = $next($request);
+
+ return new Response(
+ $response->getContent(),
+ $response->getStatusCode(),
+ $response->getHeaders() + ['X-Api' => 'Shift']
+ );
+});
+
+ Middleware may be a class string, an object implementing MiddlewareInterface, or a callable. Class strings are resolved from the service container when registered there.
Service Container
+Shift\Service\ServiceContainer stores regular services and singletons. It can resolve closures, class names, and already-created objects.
$container->register(UserRepository::class, UserRepository::class);
+$container->singleton(HealthService::class, HealthService::class);
+$container->singleton('request', $request);
+
+$service = $container->resolve(HealthService::class);
+$exists = $container->has(HealthService::class);
+
+ The app registers the current request and router as default singleton services under request and router.
CLI
+The CLI entry point is shift.php. Built-in commands live under Shift\Console\Commands, and module commands are loaded from module command mappings.
php shift.php route:list
+php shift.php health
+
+ 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
+ }
+}
+
+ 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 current lightweight test suite is in tests/ApiCoreTest.php.
composer test
+
+ The API workflow also validates Composer configuration, dumps autoload files, lints PHP files, runs the API tests, and verifies the route list command.
+