php-concept / core
Concept Framework Core Engine
Requires
- php: ^8.4
- cuyz/valinor: ^2.4
- filp/whoops: ^2.18
- hassankhan/config: ^3.2
- illuminate/database: ^13.1
- illuminate/events: ^13.1
- illuminate/filesystem: ^13.1
- illuminate/pagination: ^13.1
- laminas/laminas-diactoros: ^3.8
- laminas/laminas-httphandlerrunner: ^2.13
- league/container: ^5.1
- league/plates: ^3.6
- league/route: ^6.2
- magewirephp/validation: ^1.0
- monolog/monolog: ^3.10
- symfony/console: ^8.0
- symfony/filesystem: ^8.0
- symfony/http-foundation: ^8.0
- twig/twig: ^3.0
- vlucas/phpdotenv: ^5.6
Requires (Dev)
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^13.1
README
Concept Core is an aesthetic, strict-typed PHP engine built on PSR standards.
A strict-typed PHP application engine for builders who want ergonomics without the monolith.
Concept Core is the foundation of the Concept stack: a lean, PSR-native runtime that wires routing, DI, validation, database access, views, and CLI into a coherent whole — then gets out of your way. Applications are assembled from components: self-contained modules that ship their own routes, migrations, seeders, views, assets, and service providers.
Built for PHP 8.4+, every public API uses declare(strict_types=1) and is designed to be extended, not overridden.
No magic — no hidden conventions you cannot grep for.
Why Concept Core?
| You get | Without |
|---|---|
| Named routes, middleware, FormRequests, DTO casting | Re-implementing half of a framework on every project |
| Illuminate Database (Eloquent, migrations, pagination) | Dragging in all of Laravel |
| Twig or Plates, your choice | Lock-in to one templating story |
| Component registry — drop in Auth, ACL, CMS as packages | Copy-pasting modules between repos |
| PSR-7 HTTP, PSR-11 container, PSR-15 middleware | Fighting legacy globals |
| Whoops in dev, safe fallbacks in prod | White screens and silent failures |
| Built-in telemetry hooks | Guessing what booted and when |
Concept Core is not a CMS and not a full-stack framework. It is an engine: opinionated where it matters (request lifecycle, validation, errors), flexible everywhere else (authorization, business logic, UI).
Architecture at a glance
flowchart TB
subgraph bootstrap [Bootstrap]
App[App::create]
DI[League Container + Reflection]
Providers[Service Providers]
Components[Component Registry]
App --> DI
App --> Providers
Providers --> Components
end
subgraph http [HTTP Request]
Request[PSR-7 Request]
Router[League Route]
MW[Middleware Stack]
Strategy[RouteStrategy]
Interceptors[Route Interceptors]
Resolve[Autowire: FormRequest, route params, DI]
Controller[Controller]
Response[PSR-7 Response]
Emitter[SapiEmitter]
Request --> Router --> MW --> Strategy
Strategy --> Interceptors --> Resolve --> Controller
Controller --> Response --> Emitter
end
subgraph support [Core Services]
Config[Noodlehaus Config]
Validator[Rakit Validation]
Caster[Valinor Mapper]
DB[Illuminate Capsule]
View[Twig / Plates]
end
Components -->|routes| Router
Providers -->|register| support
Providers --> DI
Resolve --> Validator
Resolve --> Caster
Controller --> View
Controller --> DB
Loading
Request flow
Appboots service providers;ComponentsServiceProviderregisters component routes, migrations, seeders, views, and commands.Routermatches a route; middleware runs (CSRF, validation handler, view data, …).RouteStrategyinvokes the handler: route interceptors first, then autowiring (FormRequest validation, route-param casting,ServerRequestInterface).- Controller returns a PSR-7 response;
SapiEmittersends it.
Tech stack
| Layer | Library |
|---|---|
| HTTP messages | laminas/laminas-diactoros |
| HTTP runner | laminas/laminas-httphandlerrunner |
| Routing | league/route |
| DI | league/container |
| Database | illuminate/database |
| Validation | rakit/validation (via magewirephp/validation) |
| DTO mapping | cuyz/valinor |
| Views | twig/twig or league/plates |
| Console | symfony/console |
| Sessions & flash | symfony/http-foundation |
| Config | hassankhan/config |
| Logging | monolog/monolog |
| Errors (dev) | filp/whoops |
| Env | vlucas/phpdotenv |
Quick start
Install
composer require php-concept/core
Requires PHP 8.4+ and extensions: pdo, json, mbstring, dom, xml.
Minimal bootstrap
<?php declare(strict_types=1); use Concept\Core\App; $rootPath = dirname(__DIR__); $paths = include $rootPath . '/bootstrap/paths.php'; $app = App::create($rootPath, $paths); $app->registerServiceProviders([ $rootPath . '/bootstrap/providers/app.php', ]); return $app;
Front controller (public/index.php)
<?php declare(strict_types=1); $app = require dirname(__DIR__) . '/bootstrap/app.php'; $app->run();
Console entry (bin/console.php)
<?php declare(strict_types=1); use Symfony\Component\Console\Application as ConsoleApplication; $app = require dirname(__DIR__) . '/bootstrap/app.php'; $app->registerServiceProviders([dirname(__DIR__) . '/bootstrap/providers/console.php']); /** @var ConsoleApplication $console */ $console = $app->getContainer()->get(ConsoleApplication::class); exit($console->run());
Components — the killer feature
A component is a vertical slice of your application: routes, providers, migrations, seeders, views, console commands, and publishable assets — bundled behind one class.
<?php declare(strict_types=1); namespace Concept\Components\AuthAdmin; use Concept\Core\Services\Component\Contracts\ComponentInterface; use Concept\Core\Services\Database\Contracts\SeederInterface; use League\Container\ServiceProvider\ServiceProviderInterface; use Symfony\Component\Console\Command\Command; class AuthAdminComponent implements ComponentInterface { public function name(): string { return 'auth-admin'; } public function version(): string { return '1.0.0'; } public function description(): string { return 'Admin authentication module'; } public function routes(): ?string { return __DIR__ . '/routes.php'; } /** @return class-string<ServiceProviderInterface>[] */ public function providers(): array { return []; } /** @return class-string[] */ public function viewExtensions(): array { return []; } /** @return array<string, string> */ public function viewPaths(): array { return ['auth-admin' => 'src/Components/AuthAdmin/Views']; } /** @return array<string, string> */ public function viewContexts(): array { return ['/admin' => 'dashboard']; } /** @return class-string<Command>[] */ public function commands(): array { return []; } /** @return class-string<SeederInterface>[] */ public function seeders(): array { return []; } /** @return list<string> migration directory paths */ public function migrations(): array { return [__DIR__ . '/Database/Migrations']; } /** @return array<string, string> */ public function assets(): array { return []; } }
Register the component class in config (resolved from the container via ReflectionContainer):
// config/components.php return [ 'components' => [ AuthAdminComponent::class, ], ];
ComponentsServiceProvider automatically:
- loads each component's
routes()file into the router; - registers component service providers;
- merges migration paths and seeder classes into registries;
- attaches Twig extensions and view namespaces;
- registers console commands;
- maps URL prefixes to view contexts (e.g.
/admin→dashboardlayout).
ComponentInterface methods:
| Method | Returns |
|---|---|
providers() |
Extra DI service provider classes |
migrations() |
Migration directory paths |
seeders() |
Seeder class names |
commands() |
Symfony console command classes |
viewPaths() |
Twig/Plates namespaces (@auth-admin/...) |
viewContexts() |
URI prefix → layout namespace |
viewExtensions() |
Twig extension classes |
assets() |
Source → public publish map |
Ship a feature once. Reuse it in every project.
HTTP layer
Routing
Routes live in plain PHP files. League Route gives you groups, middleware, and named routes:
$router->group('/admin/users', function (RouteGroup $route) { $route->get('/', [UserController::class, 'index'])->setName('admin.users'); $route->post('/store', [UserController::class, 'store'])->setName('admin.user.store'); })->lazyMiddleware(AuthMiddleware::class);
Generate URLs anywhere:
$url = $urlGenerator->uri('admin.user.edit', ['id' => 5]);
List routes from CLI:
php bin/console.php route:list php bin/console.php route:list --full-middleware
RouteStrategy — autowiring that actually helps
The controller instance is resolved from the container (constructor injection via ReflectionContainer). Action method parameters are filled by RouteStrategy:
| Parameter type | Resolved from |
|---|---|
FormRequest subclass |
Container instance, validated, injected (throws ValidationException on failure) |
ServerRequestInterface |
Current PSR-7 request (route vars already attached) |
Route vars (int $id) |
URL segment, cast via Valinor (CasterInterface) |
| Parameters with defaults | Default value |
| Anything else | null |
No manual $request->getAttribute('id') boilerplate for route parameters.
Route interceptors
Pre-controller hooks configured in config/routes.php:
return [ 'routes' => [ 'interceptors' => [ AclRouteAuthorization::class, // implement RouteInterceptorInterface ], 'list' => [__DIR__ . '/../routes/web.php'], ], ];
Throw your own exceptions; core stays agnostic. Pair with middleware like HandleValidationExceptionMiddleware for redirects and JSON errors.
Built-in middleware
| Middleware | Role |
|---|---|
VerifyCsrfTokenMiddleware |
CSRF verification on mutating requests |
HandleValidationExceptionMiddleware |
Redirect back with errors / JSON 422 |
ShareViewDataMiddleware |
Flash, old input, CSRF token → view context |
StorePreviousUrlMiddleware |
Tracks previous/current URL in session; sets safe_back_url for ResponseFactory::back() |
ParseJsonBodyMiddleware |
JSON request bodies |
ForceJsonResponseMiddleware |
API-style error responses |
Response factory
return $this->response->json(['users' => $users]); return $this->response->jsonSuccess($data); return $this->response->jsonError('Not found', 404); return $this->response->redirectByName('admin.users'); return $this->viewResponse->create('@dashboard/index', ['title' => 'Home']);
Validation & DTOs
FormRequest
/** @extends FormRequest<StoreUserDto> */ class StoreUserRequest extends FormRequest { protected ?string $dtoClass = StoreUserDto::class; protected array $except = ['password_confirmation']; public function rules(): array { return [ 'name' => ['required', 'min:3'], 'email' => ['required', 'email', 'unique:users,email'], 'password' => ['required', 'min:8'], ]; } }
In the controller:
public function store(StoreUserRequest $request): ResponseInterface { $dto = $request->toDto(); // Valinor-mapped immutable DTO $data = $request->validated(); // plain array // ... }
Features:
- Rakit rules with custom rule registration via config;
- localized messages from
{validator_translations}/{locale}.php(path mapped inbootstrap/paths.php, typicallyresources/lang/validator); - field aliases and per-request message overrides;
- automatic CSRF field exclusion;
- optional validation payload logging (masked).
Valinor casting
Route parameters and DTOs are mapped with Valinor — cached mappers, scalar coercion, superfluous key tolerance. Route-param cast failures throw CastingException; DTO mapping via FormRequest::toDto() wraps failures as ValidationCastException.
Database
Illuminate Capsule + Eloquent without the Laravel kernel:
- fluent query builder and Eloquent models;
- migrations via
Illuminate\Database\Migrations\Migrator; - seeders aggregated from app + components;
- pagination wired to the current request URI;
- optional SQL query logging to Monolog.
CLI
php bin/console.php db:migrate
php bin/console.php db:rollback
php bin/console.php migration:list
php bin/console.php db:seed
php bin/console.php db:seed -c "Concept\Components\AuthAdmin\Database\Seeders\UserSeeder"
php bin/console.php seeders:list
Migrations and seeders from all registered components are merged automatically.
Views
Switch engines via service provider — register one of:
TwigServiceProvider(.twig, compiled cache in production)PlatesServiceProvider(.phptemplates)
Namespaces & contexts
App-level config (config/view.php) or component viewPaths() / viewContexts() / viewExtensions():
// config/view.php (app-level) return [ 'view' => [ 'paths' => [ 'dashboard' => 'resources/views/dashboard', ], 'contexts' => [ '/admin' => 'dashboard', // auto-select layout namespace by URL prefix ], 'extensions' => [ UrlGeneratorTwigExtension::class, // your Twig extension ], ], ];
Render:
{% extends '@dashboard/layouts/base.twig' %}
{# URL helpers require a Twig extension you register in view.extensions #}
<a href="{{ uri('admin.users') }}">Users</a>
ShareViewDataMiddleware injects errors, old, flashes, and csrf_token into every view.
php bin/console.php view:clear
Configuration
PHP config files merged via Noodlehaus Config. Canonical keys live in Concept\Core\Foundation\ConfigKey:
app.name,app.debug,app.version,app.locale,app.fallback_locale,app.timezonedb.*,session.*,log.*(log.db_queries,log.validation_data)routes.list,routes.interceptorscomponents,commands,migrations.paths,migrations.table,seeders.listview.paths,view.contexts,view.extensions,view.cache_dir,view.default_extensionvalidator.rules,masking.*,telemetry.enabled
.env support through vlucas/phpdotenv in the application skeleton.
Security
- CSRF — token generation, session storage, middleware verification, shared as
csrf_tokenview global. - Session — secure cookie options (lifetime,
SameSite,HttpOnly, strict mode). - Data masker — redact passwords, tokens, and custom patterns before they hit logs.
- Validation whitelist —
validated()returns only keys defined inrules().
Authorization (ACL, policies, gates) is intentionally application-level via route interceptors and middleware — core provides the hook, you bring the rules.
Error handling
Errors are handled from the first line of bootstrap:
| Environment | Behaviour |
|---|---|
APP_DEBUG=true |
Whoops Pretty Page (web) |
| CLI | Plain text stack traces |
| Production web | Branded fallback from errors_fallback_views path (bootstrap/paths.php; typically resources/views/errors/fallback) |
| JSON requests | Whoops JsonResponseHandler when RequestFormat::expectsJson() |
All paths log through PhpErrorLogHandler → Monolog. Before providers boot, early failures use resources/views/errors/fallback/500.php. Production handler never leaks stack traces to users.
Telemetry
When telemetry.enabled is true, the framework records bootstrap milestones:
- service provider awakening;
- component registration;
- per-route controller invocation timing.
Useful for debug bars and performance profiling. Access via TelemetryCollector in the container.
Logging
Monolog with rotating file handler (storage/logs). Configurable level, retention, and channel name. Database queries and validation payloads can be logged with automatic masking.
Locale
LocaleServiceProvider resolves the active locale from config or a custom LocaleResolverInterface (app.locale_resolver). Validation messages load from {validator_translations}/{locale}.php with app.fallback_locale fallback.
Service providers (built-in)
Providers live under Concept\Core\Providers\{Group}\*.
Providers/
├── Bootstrap/ ConfigServiceProvider, ErrorHandlerServiceProvider
├── Database/ DatabaseServiceProvider
├── Http/ HttpServiceProvider, SessionServiceProvider
├── Logging/ LogServiceProvider, DebugLoggerServiceProvider
├── View/ TwigServiceProvider, PlatesServiceProvider, ViewRegistryServiceProvider
├── Console/ ConsoleServiceProvider
├── Support/ CastingServiceProvider, ValidationServiceProvider, LocaleServiceProvider, DataMaskerServiceProvider
├── Telemetry/ TelemetryServiceProvider
└── Component/ ComponentsServiceProvider
| Provider | Registers |
|---|---|
Bootstrap\ConfigServiceProvider |
Config, PathManager, .env |
Http\HttpServiceProvider |
Router, PSR-7 request, URL generator, responses |
Http\SessionServiceProvider |
Symfony session & flash bag |
Support\ValidationServiceProvider |
Rakit validator, translations |
Support\CastingServiceProvider |
Valinor CasterInterface |
Database\DatabaseServiceProvider |
Capsule, migrator, seeder manager |
View\TwigServiceProvider / View\PlatesServiceProvider |
View engine |
View\ViewRegistryServiceProvider |
Paths, extensions, contexts |
Component\ComponentsServiceProvider |
Component boot (routes, migrations, …) |
Console\ConsoleServiceProvider |
Symfony Console application |
Logging\LogServiceProvider |
Monolog |
Bootstrap\ErrorHandlerServiceProvider |
Whoops handlers per environment |
Telemetry\TelemetryServiceProvider |
Telemetry collector |
Support\DataMaskerServiceProvider |
Log redaction rules |
Support\LocaleServiceProvider |
Locale resolution |
Logging\DebugLoggerServiceProvider |
In-memory debug log (dev tools) |
Register only what you need in bootstrap/providers/app.php.
Console commands (core)
| Command | Description |
|---|---|
route:list |
All registered routes, middleware, handlers |
db:migrate |
Run pending migrations |
db:rollback |
Roll back last batch |
migration:list |
Recent migrations from the database table |
db:seed |
Run seeders (all or -c ClassName) |
seeders:list |
Registered seeder classes |
component:list |
Installed components |
component:publish-assets |
Copy component assets to public/ |
view:clear |
Clear compiled Twig cache |
Components can register their own commands the same way.
Development
composer test # PHPUnit composer phpstan # Static analysis composer test:coverage # PCOV coverage report
The core ships with 70+ test classes covering routing, validation, providers, middleware, Whoops integration, and component boot sequences.
Design principles
- Strict types everywhere — fewer surprises at runtime.
- PSR first — interoperable HTTP, container, middleware.
- Composition over inheritance — components and providers, not base controller magic.
- Explicit configuration — no hidden conventions you cannot grep for.
- Fail loud in dev, safe in prod — Whoops vs fallback views.
- Batteries included, swappable — Twig or Plates.
Project layout (application skeleton)
├── bin/console.php # CLI entry
├── bootstrap/
│ ├── app.php # App::create + providers
│ ├── paths.php # PathName → relative dirs
│ └── providers/
│ ├── app.php # Web service providers
│ └── console.php # + ConsoleServiceProvider
├── config/ # PHP config files
├── routes/web.php # Application routes
├── src/
│ ├── App/ # App-specific code
│ └── Components/ # Feature modules
├── resources/views/ # Global templates
├── database/migrations/ # App-level migrations
└── public/index.php # Front controller
Read the Documenation
License
MIT © Concept Framework contributors.
Concept Core — small kernel, sharp edges, infinite composition.
Build the app. Ship the component. Repeat.