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
2 changes: 2 additions & 0 deletions Engine/Console/Commands/RouteList.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Engine\Console\Cli;
use Engine\Console\CommandInterface;
use Engine\Modules\ModuleLoader;
use Engine\Router;

class RouteList implements CommandInterface
Expand All @@ -12,6 +13,7 @@ public function execute(mixed ...$args): void
{
$cli = new Cli();
$router = new Router();
(new ModuleLoader())->load()->registerRoutes($router);
$routesFile = APP_PATH . '/routes.php';

if (!file_exists($routesFile)) {
Expand Down
6 changes: 5 additions & 1 deletion Engine/Console/Shift.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

namespace Engine\Console;


use Engine\Modules\ModuleLoader;
use ReflectionClass;
use ReflectionException;

Expand Down Expand Up @@ -70,6 +70,10 @@ public function run(): void
'namespace' => 'Console\\Commands\\'
],
];
$mappings = array_merge(
$mappings,
(new ModuleLoader())->load()->getCommandMappings()
);
$found = false;
foreach ($mappings as $mapping) {
if (!$found && file_exists($mapping['dir'] . $commandName . '.php')) {
Expand Down
22 changes: 22 additions & 0 deletions Engine/Modules/AbstractModule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace Engine\Modules;

use Engine\Router;
use Engine\ServiceContainer;

abstract class AbstractModule implements ModuleInterface
{
public function registerServices(ServiceContainer $container): void
{
}

public function registerRoutes(Router $router): void
{
}

public function getCommandMappings(): array
{
return [];
}
}
17 changes: 17 additions & 0 deletions Engine/Modules/ModuleInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace Engine\Modules;

use Engine\Router;
use Engine\ServiceContainer;

interface ModuleInterface
{
public function getName(): string;

public function registerServices(ServiceContainer $container): void;

public function registerRoutes(Router $router): void;

public function getCommandMappings(): array;
}
74 changes: 74 additions & 0 deletions Engine/Modules/ModuleLoader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

namespace Engine\Modules;

use Engine\Router;
use Engine\ServiceContainer;

class ModuleLoader
{
/** @var ModuleInterface[] */
private array $modules = [];

public function __construct(private readonly string $modulesPath = APP_PATH . '/modules')
{
}

public function load(): self
{
if (!is_dir($this->modulesPath)) {
return $this;
}

foreach (glob($this->modulesPath . '/*/Module.php') ?: [] as $moduleFile) {
require_once $moduleFile;

$moduleName = basename(dirname($moduleFile));
$moduleClass = 'Modules\\' . $moduleName . '\\Module';

if (!class_exists($moduleClass)) {
continue;
}

$module = new $moduleClass();

if ($module instanceof ModuleInterface) {
$this->modules[] = $module;
}
}

return $this;
}

public function registerServices(ServiceContainer $container): void
{
foreach ($this->modules as $module) {
$module->registerServices($container);
}
}

public function registerRoutes(Router $router): void
{
foreach ($this->modules as $module) {
$module->registerRoutes($router);
}
}

public function getCommandMappings(): array
{
$mappings = [];

foreach ($this->modules as $module) {
foreach ($module->getCommandMappings() as $mapping) {
$mappings[] = $mapping;
}
}

return $mappings;
}

public function getModules(): array
{
return $this->modules;
}
}
65 changes: 65 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,71 @@ List registered API routes:
php shift.php route:list
```

Run the example module command:

```sh
php shift.php health
```

## Modules

ShiftPHP supports a modular monolith structure under `application/modules`.

Each module can own its controllers, routes, services, and CLI commands:

```text
application/modules/Health/
├── Module.php
├── Controllers/
├── Services/
└── Commands/
```

A module registers itself through `Module.php`:

```php
namespace Modules\Health;

use Engine\Modules\AbstractModule;
use Engine\Router;
use Engine\Routing\AttributeRouteLoader;
use Engine\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\\',
],
];
}
}
```

Modules are loaded automatically by convention from `application/modules/*/Module.php`.

## Tests

Run the lightweight API core test suite:
Expand Down
22 changes: 22 additions & 0 deletions REFACTORING.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,30 @@ Implemented in this branch:
- `route:list` CLI command,
- PHP 8 attributes for controller routing,
- PHP 8 attributes for response metadata and parameter binding,
- modular monolith support through `application/modules/*/Module.php`,
- removal of view storage and example page assets from runtime.

## Modular Monolith Direction

Each module can own:

- controllers,
- routes,
- services,
- commands.

Module layout:

```text
application/modules/{ModuleName}/
├── Module.php
├── Controllers/
├── Services/
└── Commands/
```

`Module.php` is the module boundary. It registers services into the container, routes into the router, and command mappings into the CLI.

## Runtime Flow

```text
Expand Down
30 changes: 30 additions & 0 deletions application/modules/Health/Commands/Health.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

namespace Modules\Health\Commands;

use Engine\Console\Cli;
use Engine\Console\CommandInterface;
use Modules\Health\Services\HealthService;

class Health implements CommandInterface
{
public function execute(mixed ...$args): void
{
$cli = new Cli();
$health = new HealthService();

foreach ($health->status() as $key => $value) {
$cli->success($key . ': ' . $value);
}
}

public function getHelp(): string
{
return 'Usage: php shift.php health';
}

public function getDescription(): string
{
return 'Show health module status.';
}
}
22 changes: 22 additions & 0 deletions application/modules/Health/Controllers/HealthController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace Modules\Health\Controllers;

use Engine\Controller;
use Engine\JsonResponse;
use Engine\Routing\Attributes\Get;
use Engine\Routing\Attributes\RoutePrefix;
use Modules\Health\Services\HealthService;

#[RoutePrefix('/health')]
class HealthController extends Controller
{
#[Get('')]
public function index(): JsonResponse
{
/** @var HealthService $health */
$health = $this->getContainer()->resolve(HealthService::class);

return $this->json($health->status());
}
}
40 changes: 40 additions & 0 deletions application/modules/Health/Module.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

namespace Modules\Health;

use Engine\Modules\AbstractModule;
use Engine\Router;
use Engine\Routing\AttributeRouteLoader;
use Engine\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\\',
],
];
}
}
14 changes: 14 additions & 0 deletions application/modules/Health/Services/HealthService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace Modules\Health\Services;

class HealthService
{
public function status(): array
{
return [
'status' => 'ok',
'module' => 'health',
];
}
}
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
"Console\\": "Engine/Console",
"AppConsole\\": "application/console",
"Engine\\": "Engine",
"Controllers\\": "application/controller"
"Controllers\\": "application/controller",
"Modules\\": "application/modules"
}
}
}
4 changes: 4 additions & 0 deletions index.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?php
//error_reporting(E_ALL);
use Engine\App;
use Engine\Modules\ModuleLoader;
use Engine\Request;

error_reporting(-1);
Expand All @@ -11,6 +12,9 @@
// Create request instance and start application
$request = new Request();
$app = new App($request);
$modules = (new ModuleLoader())->load();
$modules->registerServices($app->getContainer());
$modules->registerRoutes($app->getRouter());

$routes = APP_PATH . '/routes.php';
if (file_exists($routes)) {
Expand Down
Loading
Loading