TL;DR Run one command
php artisan new:model ...→ you get Model + Repository + Service + Controller + Resource + Request + Migration + Dynamic Routes wired to powerful Traits/DSLs (search, permissions, tenancy, storage, relations, stats, caching). Most CRUD & reporting works out-of-the-box; add business logic in the Service when you need it.
-
Why this approach
-
Architecture map
-
Request → DB lifecycle
-
Quickstart
-
Core building blocks
- HasBaseModel (traits bundle)
- Repository
- Service
- Controller
- Resource & Request
- Dynamic Routes
- Job: ExecuteJob
-
The
new:modelgenerator (fields, options, examples) -
Search DSL & Statistics
-
Permissions (Entity & Special)
-
Customization (override/extend)
-
Performance & caching
-
FAQ & common pitfalls
-
Cheatsheets
- Productivity: new domains ship with almost zero manual boilerplate.
- Consistency: ~70 modules follow the same lifecycle and conventions.
- Extensibility: when something is special, you override in Repository/Service, not everywhere.
HTTP Request
↓
Dynamic Route + Middlewares (role/permissions/tenant)
↓
Generic Controller
↓
BaseService (caching, related, stats, files, permissions)
↓
BaseRepository (clean I/O, query building)
↓
Eloquent Model (HasBaseModel traits bundle)
↓
Resource (output shaping) / Request (validation)
Support Layer (DSLs inside traits):
- Search DSL (filters/sort/text/date/relations)
- Relations DSL (auto relations from *_id + preferences)
- Permissions DSL (entity + per-record special)
- Tenancy DSL (global scope + guarded writes)
- Storage DSL (files/images/qrcodes)
- Cache DSL (keys/tags + auto invalidation)
- Helpers DSL (formatting, parsing, dates…)
- Statistics DSL (time buckets & label grouping)
- Route (built dynamically) matches
/v1/admin/{resource}(and for other roles). - Middleware enforces auth/role/tenant.
- Controller (generic) gathers scopes/permissions/filters.
- Service does CRUD, related endpoints, permissions ops, files, stats; calls Repository for reads/writes.
- Repository builds the query & normalizes payloads.
- Model (HasBaseModel) provides: search, relations, tenancy, storage, permissions, logs.
- Resource formats JSON (with translations & relations). Request validates.
- Writes clear tagged caches; Reads use
remember().
Generate a fully wired module:
php artisan new:model Product \
--fields="name:string:translation=true:tiny=true; code:string:unique; status:enum:values=[valid,invalid,none]; owner_id:integer:default=0" \
--options="image=true; files=true; permissions=true; model=true; controller=true; request=true; resource=true; repository=true; service=true; migration=true"Run migrations:
php artisan migrateYour admin endpoints are live under:
/v1/admin/products
You’ll have: index, store, show, update, delete, delete.all, statistics, download, permissions/*, files, image, related/*, ...
Every model use HasBaseModel; gets:
-
HasFillable: generates
fillable,casts,searchable,filterablefrom the DB schema. -
HasRelations (+Deep): auto-discovers relations from
*_idcolumns + relation preferences to steer chains. -
HasMultiTenancy: global scope on tenant column (default
store_id); guarded create/delete across tenants; joins are tenant-aware. -
HasFileStorage: upload/delete files, single image helpers, QR code support.
-
Permissions Workflow:
- Entity permissions (public per module)
- Special permissions (per record override)
-
Search Workflow: Builder macros (
getResource(),getStats(), …) + unified search DSL. -
Statistics: time-bucket aggregations (day/week/month/quarter/year/7years) & label counts.
-
Booted Logs: on
created/updatedhandle files, toggle active, dispatch logs/notifications if user is present.
Disable features per model with:
protected array $disabledTraits = ['relations', 'tenancy', ...];
- Standard API:
query(), findOrFail(), create(), update(), delete(), updateOrCreate(), dbTransaction(...). fields(array $data)to normalize/shape payloads before writes.- Respects tenancy/scopes automatically.
- CRUD:
index, show, store, update, delete, deleteMultiple, setDeleted… - Related endpoints:
related, showRelated, statisticsRelated, downloadRelated(for any relation name). - Caching helpers:
remember/successRemember/successDownload/deleteCacheTagusing model-derived tags. - Async jobs: dispatch
ExecuteJobfor background work. - Hooks:
initialize()/boot()to set default cache tag/key/minutes per service. - Writes always call
deleteCache()for the relevant tag.
- Applies default scopes and permissions per role (admin/vendor/client/guest).
- Delegates all CRUD/related/permissions/files to the Service.
- Dynamic
related{Relation}()methods are resolved automatically.
-
Resource:
data()+tiny()with auto-localized fields .- Embeds image/files/qrcodes/relations/permissions/translation if enabled.
-
Request:
- Builds validation rules from fields (required/unique/enum/exists/…).
- A route registrar loops over
App\Http\Controllers\*and registers standardized endpoints using controller metadata (e.g.global_route,route_name). - Covers CRUD, related, permissions, attachments, stats, downloads.
- Production-ready with
php artisan route:cache.
- Runs any callback Closure /
[Class, method]/"Class@method"inside the correct tenant (store_id) context. - Good for logs/notifications/expensive tasks.
Outputs: Model, Repository, Service, Controller, Resource, Request, Migration. Prevents overwriting existing files; creates folders when missing.
name:string:translation=true:tiny=true:label=Titlecode:string:uniquestatus:enum:values=[valid,invalid,none]owner_id:integer:default=0:indexrelated:morph(polymorphic)price:decimal(10,2):default=0city_id:integer→ Resource auto-linkscityrelation (and will useCityResource::infoif found).
image=trueorfiles=trueto include image/files helpers in Resource.permissions=true,multitenancy=true,softdelete=true,timestamp=true.- Toggle file generation:
model=false,resource=false, etc.
- Migration: parses
type,default,index/unique,enum(values=[]),morph,decimal(precision,scale)… - Request: builds rules from field metadata (required/unique/exists/enum…).
- Resource: generates consistent
data()andtiny()with optional --. - Routes: picked up automatically by the route loop.
Single entry point via query params:
?search=...&filters[...]&sort=...&page=&limit=
- Numbers match
iddirectly. - Otherwise tokenized LIKE across
searchablecolumns (from schema or overrides).
-
Equality:
status@=valid -
Ranges:
price@between=[10,100],price@notin=[5,9] -
Dates:
- Keywords:
created_at@thismonth,@today,@yesterday,@lastweek,@expired,@notexpired - Range:
created_at@2024-01..2024-02 - Granular:
@=2024-01-02 12:30matches the minute; also supportsyear,time, weekdays
- Keywords:
-
LIKE:
name@like=pro max(tokenized) -
Relations:
user.email@like=gmail.com -
Nullability:
deleted=nullordeleted=not null -
Set ops:
@in,@notin
sort=newest|oldest|{column}@asc|{column}@desc
period:day|week|month|quarter|year|7yearstype:period(count) orlabel(group by column) orsum/avg/max/minfor a column- Auto fills missing buckets.
Two layers:
-
Entity (public per module)
- Defined via config & synced with
syncPermissions() - APIs:
Model::allowPermission('export'),denyPermission,allowedEntityPermissions(), …
- Defined via config & synced with
-
Special (per record)
item->allow('publish')/item->deny('delete')allowedPermissions()/deniedPermissions()- Access check merges public + special with a clear rule (special overrides).
Routes can require permissions via middleware like has:permission_name.
-
Service → where business rules live (e.g.,
ProductService::order,coupon,checkout). Keep generic lifecycle in BaseService. -
Repository → normalize payloads in
fields(); addboot/createBoot/updatedBoothooks if needed. -
Controller → override
requestForm()to use a custom Request for specific actions. -
Model → steer relations with:
protected array $relationPreference = [ 'country' => 'category.city.country', 'city' => 'category.city', ];
Disable features with
$disabledTraits.
- Route caching:
php artisan route:cache. - Reads:
remember()/successRemember()with tag/key/minutes configured in Service. - Writes: always invalidate via
deleteCache(). - N+1: set
withRelationsandwithAggregatesin the Model for default eager loads/aggregations. - Tenancy: enforced on base queries and joined tables when they share the tenant column.
-
Why dynamic routes? To remove boilerplate and guarantee consistency across ~70 modules. With route caching, overhead is negligible.
-
Onboarding a new dev? Read this README, open any generated module—models look “empty” by design because power lives in traits. Custom behavior goes to Service/Repository.
-
How to disable a feature per model?
$disabledTraits(e.g.['relations']). -
Debugging? Business logic is in Services (thin, explicit). Shared behavior is tested once in bases/traits. Logs +
ExecuteJobhelp trace background work. -
Custom endpoints? Add a method in Service, then a thin wrapper in Controller. For relations, sticking to the naming convention lets routes resolve automatically.
GET /v1/admin/{resource} → index
POST /v1/admin/{resource} → store
GET /v1/admin/{resource}/{id} → show
PUT /v1/admin/{resource}/{id}/{column?} → update (single column or full)
DELETE /v1/admin/{resource}/{id} → delete
DELETE /v1/admin/{resource} → deleteMultiple (ids[])
GET /v1/admin/{resource}/statistics → statistics
POST /v1/admin/{resource}/download → download
POST /v1/admin/{resource}/{id}/image → updateImage
DELETE /v1/admin/{resource}/{id}/image → deleteImage
POST /v1/admin/{resource}/{id}/files → uploadFiles
DELETE /v1/admin/{resource}/{id}/files → deleteFiles
GET /v1/admin/{resource}/{id}/permissions → allPermissions
POST /v1/admin/{resource}/{id}/permissions/allow → allowPermission
POST /v1/admin/{resource}/{id}/permissions/deny → denyPermission
# generic related
GET /v1/admin/{resource}/{id}/{related} → related{Related}()
GET /v1/admin/{resource}/{id}/{related}/{relId} → showRelated{Related}()
GET /v1/admin/{resource}/{id}/{related}/statistics → statisticsRelated{Related}()
POST /v1/admin/{resource}/{id}/{related}/download → downloadRelated{Related}()
?search=iphone
&sort=newest # or oldest or price@asc
&page=1&limit=20
&filters[status@=]=valid
&filters[price@between]=[10,100]
&filters[created_at@thismonth]=1
&filters[user.email@like]=gmail.com
&filters[deleted]=not null
class Order extends Model {
use HasBaseModel;
protected array $disabledTraits = ['relations']; // example
protected array $relationPreference = [
'country' => 'city.country',
];
}class ProductRepository extends BaseRepository {
public function fields(array $data = []){
$d = optional($data);
return [
'name' => $d['name'],
'price' => float($d['price']),
'city_id' => integer($d['city_id']),
// ...
];
}
}
class ProductService extends BaseService {
public function __construct(
protected ProductRepository $productRepository,
protected OrderService $orderService,
// ...
){
parent::__construct($productRepository);
}
public function order(int $id, array $scopes = []){
// custom business flow
}
}This codebase is a meta-framework on top of Laravel: it gives you ~95% of a module’s lifecycle for free. The remaining ~5% (business logic) lands cleanly in Services/Repositories. Keep modules “empty” on purpose, and let the bases/DSLs do the heavy lifting.
If you need something special:
- put custom business rules in Service,
- shape inputs in Repository,
- steer relations or toggle features in the Model,
- and rely on the shared bases for the rest.
Happy shipping 🚀