Skip to content

Commit 36ba25f

Browse files
Karim-Ashrafcursoragent
authored andcommitted
Add BC aliases, feature/wizard commands, architect.json, lint and analyze
Phase A: - Deprecated Base* aliases (BaseRepository, BaseService, BaseData, BaseFormRequest, Action, QueryFilter) extending the Architect* classes so pre-1.2 apps survive composer update - policy, seeder and test patterns with generators and stubs - architect:feature command (make:module + feature_extras) - architect:new interactive wizard - architect.json project-level config overrides (TeamConfig) - {module} namespace placeholder for domain/modular layouts Phase B: - architect:lint with four convention rules (LintRule contract, configurable via lint.rules), exit 1 on violations for CI - architect:analyze layer counts + hotspot report with configurable thresholds Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 67e5824 commit 36ba25f

36 files changed

Lines changed: 1451 additions & 7 deletions

CHANGELOG.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [1.3.0] - 2026-07-22
11+
12+
### Added
13+
14+
- Backwards-compatibility aliases for the classes renamed in 1.2.0: `BaseRepository`, `BaseService`, `BaseData`, `BaseFormRequest`, `Action` and `QueryFilter` still resolve (deprecated, removal planned for 2.0), so existing apps keep working after `composer update`.
15+
- `policy`, `seeder` and `test` patterns with matching generators and stubs.
16+
- `architect:feature` command — `make:module` plus the `generation.feature_extras` patterns (policy, seeder, feature test by default).
17+
- `architect:new` interactive wizard that asks for name, preset, UI, fields and extras, then generates the module.
18+
- `architect.json` project-level config: a JSON file at the application root that deep-merges over the package config, so teams can version their conventions.
19+
- `{module}` placeholder in namespace config for domain/modular layouts (e.g. `App\Domain\{module}\Services`).
20+
- `generation.user_model` config used by generated policies.
21+
- `architect:lint` command — CI-friendly convention checker (exit code 1 on violations) with four built-in rules: no Eloquent/DB calls in controllers, no repositories injected into controllers, no inline validation in controllers, and models must not depend on the HTTP/service layer. Rules implement `Contracts\LintRule` and are registered in `lint.rules`, so teams can add their own.
22+
- `architect:analyze` command — read-only report of layer counts (controllers, models, services, repositories, actions, form requests) and hotspots: classes exceeding the configurable `lint.thresholds` for public methods, constructor dependencies and file length.
23+
1024
## [1.2.0] - 2026-07-22
1125

1226
### Added
@@ -70,7 +84,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7084
- Support for Laravel 11, 12 and 13 (PHP 8.2 – 8.5, per framework requirements).
7185
- Full test suite (PHPUnit via Orchestra Testbench), PHPStan level 5 (Larastan) and Laravel Pint.
7286

73-
[Unreleased]: https://github.com/gubakareem/lara-architect/compare/v1.2.0...HEAD
87+
[Unreleased]: https://github.com/gubakareem/lara-architect/compare/v1.3.0...HEAD
88+
[1.3.0]: https://github.com/gubakareem/lara-architect/releases/tag/v1.3.0
7489
[1.2.0]: https://github.com/gubakareem/lara-architect/releases/tag/v1.2.0
7590
[1.1.0]: https://github.com/gubakareem/lara-architect/releases/tag/v1.1.0
7691
[1.0.0]: https://github.com/gubakareem/lara-architect/releases/tag/v1.0.0

README.md

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
# LaraArchitect
22

3-
An architecture toolkit for Laravel. It gives you two things:
3+
An architecture toolkit for Laravel. It gives you three things:
44

55
1. **Runtime building blocks** — lean base classes for repositories (with full soft-delete support), CRUD services, single-purpose actions, request-driven query filters, data transfer objects, form requests and JSON responses, so business logic stays out of your controllers.
6-
2. **A dynamic module generator** — one `make:module` command that scaffolds an entire feature (model, migration, factory, enums, service/actions, filter, requests, resource, controller) using **configurable architecture presets**. Prefer actions over services? Repository pattern? Your own preset? It is all config.
6+
2. **A dynamic module generator** — one `make:module` command (plus `architect:feature` for policy/seeder/test, and the `architect:new` interactive wizard) that scaffolds an entire feature using **configurable architecture presets**. Prefer actions over services? Repository pattern? Domain layout? Your own preset? It is all config — versionable per team via `architect.json`.
7+
3. **Architecture guardrails**`architect:lint` fails CI when conventions are broken (Eloquent in controllers, inline validation, inverted dependencies) and `architect:analyze` reports layer counts and hotspots.
78

89
```bash
910
php artisan make:module Product --fields="name:string, price:decimal, sku:string:unique, status:enum, notes:text:nullable"
@@ -53,7 +54,11 @@ Published stubs live in `stubs/lara-architect/` and always win over the package
5354
php artisan architect:patterns
5455
```
5556

56-
**2.** Generate a module (add `--dry-run` first to preview without writing). Default is **API** (JsonResource + `Http\Controllers\Api`). Use `--ui=web` for Blade:
57+
**2.** Generate a module (add `--dry-run` first to preview without writing). Default is **API** (JsonResource + `Http\Controllers\Api`). Use `--ui=web` for Blade. Not sure which flags you want? Run the interactive wizard instead:
58+
59+
```bash
60+
php artisan architect:new
61+
```
5762

5863
```bash
5964
# API (default) — controller under App\Http\Controllers\Api + ProductResource
@@ -120,8 +125,16 @@ php artisan make:module Order --architecture=actions --fields="total:decimal, st
120125

121126
# Or hand-pick patterns — no preset needed
122127
php artisan make:module Tag --patterns=model,migration,resource,controller --fields="name:string:unique"
128+
129+
# Complete feature: preset patterns + policy + seeder + feature test
130+
php artisan architect:feature Product --fields="name:string, price:decimal"
131+
132+
# Interactive wizard — answers a few questions, then generates
133+
php artisan architect:new
123134
```
124135

136+
`architect:feature` accepts the same flags as `make:module` and appends the patterns listed in `generation.feature_extras` (`policy`, `seeder`, `test` by default), so one command ships a model with a passing test, a seeder wired to the factory, and a policy ready to register.
137+
125138
Other useful flags:
126139

127140
| Flag | Effect |
@@ -193,6 +206,57 @@ Every pattern is a class implementing `KarimAshraf\LaraArchitect\Contracts\Gener
193206
php artisan make:module Invoice --architecture=my-team-style
194207
```
195208

209+
### Team conventions with `architect.json`
210+
211+
Commit an `architect.json` at your project root to version your team's conventions without publishing the package config. Anything under it deep-merges over `config/lara-architect.php` when the generator runs:
212+
213+
```json
214+
{
215+
"generation": {
216+
"default_architecture": "actions",
217+
"default_ui": "api",
218+
"namespaces": {
219+
"service": "App\\Domain\\{module}\\Services",
220+
"repository": "App\\Domain\\{module}\\Repositories"
221+
}
222+
}
223+
}
224+
```
225+
226+
### Domain / modular layouts
227+
228+
Namespace values support a `{module}` placeholder that is replaced with the model name, so you can generate into a domain-oriented structure:
229+
230+
```php
231+
'namespaces' => [
232+
'service' => 'App\\Domain\\{module}\\Services', // App\Domain\Product\Services\ProductService
233+
'repository' => 'App\\Domain\\{module}\\Repositories', // App\Domain\Product\Repositories\ProductRepository
234+
],
235+
```
236+
237+
## Architecture lint & analysis
238+
239+
Generating a clean structure is half the job — keeping it clean is the other half. Two read-only commands help with that:
240+
241+
```bash
242+
# Fails (exit code 1) when conventions are broken — wire it into CI
243+
php artisan architect:lint
244+
245+
# Layer counts + hotspots (long classes, fat constructors, God services)
246+
php artisan architect:analyze
247+
```
248+
249+
`architect:lint` ships with four rules, each a class implementing `Contracts\LintRule` (register your own in `lint.rules`):
250+
251+
| Rule | Catches |
252+
| --- | --- |
253+
| `no-eloquent-in-controllers` | Controllers calling `Model::create/update/where(...)` or the `DB` facade directly |
254+
| `no-repositories-in-controllers` | Controllers injecting repositories instead of services/actions |
255+
| `no-inline-validation-in-controllers` | `$request->validate(...)` / `Validator::make(...)` instead of form requests |
256+
| `models-do-not-depend-on-http` | Models importing controllers, form requests or services |
257+
258+
`architect:analyze` reports how many controllers/models/services/repositories/actions exist and flags classes above the configurable thresholds (`lint.thresholds`): more than 8 public methods, more than 5 constructor dependencies, or files longer than 300 lines.
259+
196260
## Runtime building blocks
197261

198262
### Repository

config/lara-architect.php

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
declare(strict_types=1);
44

5+
use KarimAshraf\LaraArchitect\Analysis\Rules\ModelsDoNotDependOnHttpRule;
6+
use KarimAshraf\LaraArchitect\Analysis\Rules\NoEloquentInControllersRule;
7+
use KarimAshraf\LaraArchitect\Analysis\Rules\NoInlineValidationInControllersRule;
8+
use KarimAshraf\LaraArchitect\Analysis\Rules\NoRepositoriesInControllersRule;
59
use KarimAshraf\LaraArchitect\Generation\Generators\ActionsGenerator;
610
use KarimAshraf\LaraArchitect\Generation\Generators\ControllerGenerator;
711
use KarimAshraf\LaraArchitect\Generation\Generators\DtoGenerator;
@@ -10,10 +14,13 @@
1014
use KarimAshraf\LaraArchitect\Generation\Generators\FilterGenerator;
1115
use KarimAshraf\LaraArchitect\Generation\Generators\MigrationGenerator;
1216
use KarimAshraf\LaraArchitect\Generation\Generators\ModelGenerator;
17+
use KarimAshraf\LaraArchitect\Generation\Generators\PolicyGenerator;
1318
use KarimAshraf\LaraArchitect\Generation\Generators\RepositoryGenerator;
1419
use KarimAshraf\LaraArchitect\Generation\Generators\RequestsGenerator;
1520
use KarimAshraf\LaraArchitect\Generation\Generators\ResourceGenerator;
21+
use KarimAshraf\LaraArchitect\Generation\Generators\SeederGenerator;
1622
use KarimAshraf\LaraArchitect\Generation\Generators\ServiceGenerator;
23+
use KarimAshraf\LaraArchitect\Generation\Generators\TestGenerator;
1724
use KarimAshraf\LaraArchitect\Generation\Generators\ViewsGenerator;
1825

1926
return [
@@ -81,8 +88,20 @@
8188
'resource' => ResourceGenerator::class,
8289
'views' => ViewsGenerator::class,
8390
'controller' => ControllerGenerator::class,
91+
'policy' => PolicyGenerator::class,
92+
'seeder' => SeederGenerator::class,
93+
'test' => TestGenerator::class,
8494
],
8595

96+
/*
97+
| Extra patterns appended by `architect:feature` on top of the chosen
98+
| architecture preset, so a feature ships complete in one command.
99+
*/
100+
'feature_extras' => ['policy', 'seeder', 'test'],
101+
102+
// Model used by generated policies.
103+
'user_model' => 'App\\Models\\User',
104+
86105
/*
87106
| Target namespaces per generated class type. Paths are derived from
88107
| these namespaces (App\ => app/, Database\ => database/).
@@ -101,6 +120,9 @@
101120
'request' => 'App\\Http\\Requests',
102121
'resource' => 'App\\Http\\Resources',
103122
'factory' => 'Database\\Factories',
123+
'policy' => 'App\\Policies',
124+
'seeder' => 'Database\\Seeders',
125+
'test' => 'Tests\\Feature',
104126
],
105127
],
106128

@@ -169,4 +191,34 @@
169191
'errors' => 'errors',
170192
],
171193
],
194+
195+
/*
196+
|--------------------------------------------------------------------------
197+
| Architecture Lint & Analysis
198+
|--------------------------------------------------------------------------
199+
|
200+
| `architect:lint` checks the paths below against the registered rules
201+
| (each implements Contracts\LintRule — add your own to extend it) and
202+
| fails when violations are found, so it can run in CI.
203+
| `architect:analyze` reports layer counts and hotspots using the
204+
| thresholds below.
205+
|
206+
*/
207+
208+
'lint' => [
209+
'paths' => ['app'],
210+
211+
'rules' => [
212+
NoEloquentInControllersRule::class,
213+
NoRepositoriesInControllersRule::class,
214+
NoInlineValidationInControllersRule::class,
215+
ModelsDoNotDependOnHttpRule::class,
216+
],
217+
218+
'thresholds' => [
219+
'public_methods' => 8,
220+
'constructor_dependencies' => 5,
221+
'file_lines' => 300,
222+
],
223+
],
172224
];

docs/getting-started.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -489,4 +489,22 @@ php artisan vendor:publish --tag=lara-architect-stubs
489489
# edit stubs/lara-architect/*.stub
490490
```
491491

492-
Change target namespaces (e.g. put models in `App\Domain\Models`) under `generation.namespaces` in the config. Add your own pattern by writing a class that implements `KarimAshraf\LaraArchitect\Contracts\Generator`, registering it under `generation.generators`, and adding it to a preset — see [Extending the generator](../README.md#extending-the-generator).
492+
Change target namespaces (e.g. put models in `App\Domain\Models`) under `generation.namespaces` in the config. Namespace values support a `{module}` placeholder for domain layouts — `App\Domain\{module}\Services` generates `App\Domain\Product\Services\ProductService`. Add your own pattern by writing a class that implements `KarimAshraf\LaraArchitect\Contracts\Generator`, registering it under `generation.generators`, and adding it to a preset — see [Extending the generator](../README.md#extending-the-generator).
493+
494+
## 10. More commands
495+
496+
```bash
497+
# Interactive wizard — asks for name, preset, UI and fields, then generates
498+
php artisan architect:new
499+
500+
# make:module + policy + seeder + feature test in one go
501+
php artisan architect:feature Product --fields="name:string, price:decimal"
502+
503+
# Check the app against the architecture conventions (CI-friendly, exit 1 on violations)
504+
php artisan architect:lint
505+
506+
# Layer counts and hotspot report (fat controllers, God services, oversized files)
507+
php artisan architect:analyze
508+
```
509+
510+
Team conventions can be committed as an `architect.json` file at the project root; it deep-merges over the package config whenever these commands run — see [Team conventions with architect.json](../README.md#team-conventions-with-architectjson).

src/Actions/Action.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace KarimAshraf\LaraArchitect\Actions;
6+
7+
/**
8+
* Backwards-compatibility alias kept so applications generated before v1.2
9+
* keep working after `composer update`.
10+
*
11+
* @deprecated Use ArchitectAction instead. Will be removed in v2.0.
12+
*/
13+
abstract class Action extends ArchitectAction {}

src/Analysis/CodeScanner.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace KarimAshraf\LaraArchitect\Analysis;
6+
7+
use Illuminate\Filesystem\Filesystem;
8+
9+
class CodeScanner
10+
{
11+
public function __construct(
12+
private readonly Filesystem $files,
13+
) {}
14+
15+
/**
16+
* Scan the given paths (relative to the application root) for PHP files.
17+
*
18+
* @param list<string> $paths
19+
* @return list<ScannedFile>
20+
*/
21+
public function scan(array $paths): array
22+
{
23+
$scanned = [];
24+
25+
foreach ($paths as $path) {
26+
$absolute = base_path($path);
27+
28+
if (! $this->files->isDirectory($absolute)) {
29+
continue;
30+
}
31+
32+
foreach ($this->files->allFiles($absolute) as $file) {
33+
if ($file->getExtension() !== 'php') {
34+
continue;
35+
}
36+
37+
$scanned[] = new ScannedFile(
38+
$file->getPathname(),
39+
$this->files->get($file->getPathname()),
40+
);
41+
}
42+
}
43+
44+
return $scanned;
45+
}
46+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace KarimAshraf\LaraArchitect\Analysis\Rules;
6+
7+
use KarimAshraf\LaraArchitect\Analysis\ScannedFile;
8+
use KarimAshraf\LaraArchitect\Analysis\Violation;
9+
use KarimAshraf\LaraArchitect\Contracts\LintRule;
10+
11+
/**
12+
* Dependency direction: the HTTP layer and services may depend on models,
13+
* never the other way around. A model importing controllers, requests or
14+
* services signals inverted coupling.
15+
*/
16+
class ModelsDoNotDependOnHttpRule implements LintRule
17+
{
18+
private const FORBIDDEN_SEGMENTS = ['\\Http\\Controllers\\', '\\Http\\Requests\\', '\\Services\\'];
19+
20+
public function name(): string
21+
{
22+
return 'models-do-not-depend-on-http';
23+
}
24+
25+
public function check(ScannedFile $file): array
26+
{
27+
if (! $file->isModel()) {
28+
return [];
29+
}
30+
31+
$violations = [];
32+
33+
foreach ($file->imports as $import) {
34+
foreach (self::FORBIDDEN_SEGMENTS as $segment) {
35+
if (str_contains('\\'.$import, $segment)) {
36+
$violations[] = new Violation(
37+
$this->name(),
38+
$file->path,
39+
$file->firstLineOf($import),
40+
sprintf('Model imports [%s]; models must not depend on the HTTP or service layer.', $import),
41+
);
42+
43+
break;
44+
}
45+
}
46+
}
47+
48+
return $violations;
49+
}
50+
}

0 commit comments

Comments
 (0)