giovanni-venturelli / sophia
Sophia is a lightweight component-based PHP framework with native PHP templates.
Requires
- php: >=8.1
- ext-pdo: *
- ext-pdo_mysql: *
- ext-pdo_sqlite: *
- vlucas/phpdotenv: ^5.6
Requires (Dev)
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- dev-main
- v0.2.0
- v0.1.47
- v0.1.46
- v0.1.45
- v0.1.43
- v0.1.42
- v0.1.41
- v0.1.40
- v0.1.39
- v0.1.38
- v0.1.37
- v0.1.36
- v0.1.35
- v0.1.34
- v0.1.33
- v0.1.32
- v0.1.31
- v0.1.30
- v0.1.29
- v0.1.28
- v0.1.27
- v0.1.26
- v0.1.25
- v0.1.24
- v0.1.23
- v0.1.22
- v0.1.21
- v0.1.20
- v0.1.19
- v0.1.18
- v0.1.17
- v0.1.16
- v0.1.15
- v0.1.14
- v0.1.13
- v0.1.12
- v0.1.11
- v0.1.10
- v0.1.9
- v0.1.8
- v0.1.7
- v0.1.6
- v0.1.5
- v0.1.4
- v0.1.3
- v0.1.2
- v0.1.1
- v0.1.0
- dev-develop
- dev-feature/refactor
This package is auto-updated.
Last update: 2026-10-20 21:49:36 UTC
README
Minimal, production‑oriented PHP framework with:
- Component-based rendering with native PHP templates
- Angular‑inspired Dependency Injection system
- Simple Router (components, API callbacks, parameters, guards)
- Optional database layer with fluent QueryBuilder and Active Record ORM
Components are plain PHP classes (annotated), services are auto‑wired, routes map to components (views) or callbacks (APIs).
Quick navigation
- What you get (features)
- Architecture at a glance
- Installation
- Project structure
- Quick start (index.php + routes.php)
- Create your first component
- Dependency Injection (services)
- Routing basics (components, params, urls)
- Performance & Build System
- Database integration (optional)
- Troubleshooting
- Deep dives (module READMEs)
- Migration from Twig to native PHP
What you get (features)
- Components rendered with native PHP templates and helper functions
- Property injection in components and constructor injection in services
- Root singletons via
#[Injectable(providedIn: 'root')] - Route configuration with parameters, named routes, redirects, nested routes, guards
- Optional database service with fluent QueryBuilder and Active Record entities
Architecture at a glance
- Components:
src/Component(attributes, registry, renderer) - DI (Injector):
src/Injector(root singletons + per‑component scoped providers) - Router:
src/Router(maps requests to components or callbacks) - Database (optional):
src/Database(connection service + ORM)
Flow for a component route:
Routermatches the incoming path → selects a component classComponentRegistrylazily registers it andRenderercreates aComponentProxyComponentProxyopens a DI scope, warms providers, creates the component, runs property injection, thenonInit()Renderercollects public properties/zero‑arg getters and renders the PHP template
Installation
- PHP 8.1+
- Composer
composer require giovanni-venturelli/sophia
A .env file in the project root is loaded automatically when present.
Project structure
A typical application:
.
├─ pages/ # Your component classes and PHP templates
├─ config/
│ └─ database.php # Optional, picked up automatically
├─ routes.php # Route table
├─ index.php # App bootstrap
├─ .env # Optional, loaded automatically
└─ vendor/ # Composer dependencies (Sophia lives in vendor/giovanni-venturelli/sophia/src)
Quick start (index.php + routes.php)
Bootstrap in index.php:
<?php require __DIR__ . '/vendor/autoload.php'; Sophia\App::create(__DIR__, [ 'basePath' => 'sophia', // optional, if the app lives in a subfolder 'language' => 'it', 'styles' => ['css/style.css'], // relative to the base path 'scripts' => ['js/scripts.js'], ])->run();
Everything else is wired automatically: .env, debug mode (DEBUG in the environment),
the database (see Database integration), the router, the
renderer, the build cache in production, error pages and debug metrics.
All options, with their defaults:
| Option | Default | Meaning |
|---|---|---|
basePath |
'' |
URL prefix of the app; also used for the <base> tag and asset URLs |
debug |
DEBUG env |
Profiler, metrics and detailed errors; disables the build cache |
language |
'en' |
<html lang> |
routes |
'routes.php' |
Routes file |
components |
[] |
Directories whose #[Component] classes are registered up front (otherwise components are registered lazily) |
services |
[] |
Directories scanned by sophia build for root services |
templates |
[] |
Fallback template directories |
styles |
[] |
Global stylesheets |
scripts |
[] |
Global scripts |
favicon |
null |
Favicon path |
database |
null |
Database config array or file; null auto-detects |
Options can also live in composer.json under extra.sophia; values passed to create() win.
Settings that the build tool needs (components, routes, services) belong there, so that
sophia build sees the same values as the app:
"extra": { "sophia": { "components": ["pages"], "routes": "routes.php" } }
Define routes in routes.php by returning an array:
<?php use App\Pages\Home\HomeComponent; return [ [ 'path' => 'home/:id', // URL with param 'component' => HomeComponent::class, // Component class 'name' => 'home', // Named route 'data' => [ 'title' => 'Home Page' ], // Route-scoped data ], ];
Calling Router::getInstance()->configure([...]) inside the file still works.
Upgrading from the manual bootstrap
The old wiring (setRegistry, setRenderer, setComponentRegistry, setControllerRegistry,
configure) still works, but none of it is required anymore. Two behaviours changed:
- Global styles, scripts and the favicon are resolved against the base path when the page is
rendered, so the order of
setBasePath()andaddGlobalStyle()no longer matters. Pass paths relative to the app (css/style.css), not prefixed with the base path. DEBUG=falsein.envnow really disables debug mode (it used to be read as a non-empty string).
Create your first component
Component class under pages/... and a PHP template next to it:
<?php namespace App\Pages\Home; use Sophia\Component\Component; #[Component(selector: 'app-home', template: 'home.php')] class HomeComponent { public string $title = 'Welcome'; public string $id; public function onInit(): void { // Optionally compute public state for the template } }
Template home.php:
<h1><?= $e($title) ?></h1> <p>User ID: <?= $e($id) ?></p>
The renderer passes route params (:id) as initial data to the root component.
Dependency Injection (services)
- Mark root singletons with
#[Injectable(providedIn: 'root')]. - Use property injection in components:
#[Inject] private Service $service; - Use constructor injection in services; dependencies are resolved by the Injector.
Example service + usage in a component:
use Sophia\Injector\Injectable; use Sophia\Injector\Inject; use Sophia\Component\Component; #[Injectable(providedIn: 'root')] class Logger { public function info(string $m): void {} } #[Injectable] class UserService { public function __construct(private Logger $log) {} } #[Component(selector: 'app-users', template: 'users.php', providers: [UserService::class])] class UsersComponent { #[Inject] private UserService $users; public array $active = []; public function onInit(): void { $this->active = $this->users->getActive(); } }
See the full DI reference: Injector (DI).
Routing basics (components, params, urls)
- Define paths like
post/:idto capture params; available to components and templates - Name routes with
nameand generate URLs using theurl()helper - Provide
dataon routes; read them withroute_data()
Template helpers:
<a href="<?= $e($url('home', ['id' => 123])) ?>">Go home</a> <p>Title: <?= $e($route_data('title')) ?></p>
More details: Router.
Redirections and Guards
(Existing content for Routing basics...)
Performance & Build System
Sophia includes a build system to optimize performance in production (up to 40% faster).
How it works
In development mode (DEBUG=true), the framework scans folders and uses the Reflection API on every request. In production (DEBUG=false), the framework loads pre-generated static maps.
CLI Commands
From the project root:
# Generate structural cache (components, routes, DI) php sophia build # Clear the build cache php sophia clear
When installed as a dependency the command is vendor/bin/sophia build. Routes that contain
closures cannot be compiled: in that case the build says so and the routes file keeps being
loaded at runtime.
When to run the build?
The build is only necessary when the structure of the application changes:
- Creating new components or services.
- Modifying routes or selectors.
- Modifying
providersin Dependency Injection.
Note: It is not necessary to rebuild if you only modify the database content, the logic code inside a method, or the HTML markup of an existing template.
Database integration (optional)
The ConnectionService is a root‑provided service with a fluent QueryBuilder and an Active Record‑style ORM via Entity.
Example entity:
use Sophia\Database\Entity; class Post extends Entity { protected static string $table = 'posts'; protected static array $fillable = ['title', 'content', 'status']; }
Query examples:
$posts = Post::where('status', 'published')->orderBy('created_at', 'DESC')->limit(10)->get(); $one = Post::find(1);
Full guide: Database.
Troubleshooting
- Template not found: ensure the file exists next to the component class or in a path added to the renderer
- Undefined variable: expose data via public properties or zero‑arg getters
- Injection error: add
#[Inject]to typed component properties; mark services as#[Injectable]and/or list them inproviders - Routing mismatch: check
basePath, path normalization, and that names/params match when callingurl()
Deep dives (module READMEs)
- Components: Components
- Injector (DI): Injector (DI)
- Router: Router
- Database: Database
- Templates: Templates Guide
- Forms: Forms Guide
Using this repository as a package + demo
This repo is organized so that only the framework is published to Packagist, while the demo app stays in-repo only.
- Package (library):
giovanni-venturelli/sophia- Code in
src/, single namespaceSophia\(component, injector, router, form, database) - The
demo/folder, lint configuration andcomposer.lockare excluded from the dist via.gitattributes
- Code in
- Demo app:
demo/, a showcase of every feature, each shown live next to its own source code- Its namespace
App\(mapped todemo/app/) is declared inautoload-devof the rootcomposer.json, so it is never registered in projects that install the package
- Its namespace
Run the demo locally from this repo
composer install
With XAMPP (or any Apache with mod_rewrite) the demo is served at http://localhost/sophia/demo/
through demo/.htaccess. If you serve it at a different path, update basePath in demo/index.php
and RewriteBase in demo/.htaccess. It needs pdo_sqlite: the database file
demo/database/demo.sqlite is created and seeded on first visit.
| Section | Shows |
|---|---|
| Components | inputs and aliases, getters, onInit(), scoped CSS/JS |
| Slots | Angular-style content projection: default and named slots, fallback, nesting, slots with a context |
| Injection | root singletons, component providers, constructor injection |
| Routing | parameters, named routes, layouts with outlet, route groups, guards, redirects, ** 404 page, callbacks |
| Forms | #[FormHandler], CSRF, validation errors, old input, flash messages |
| Database | QueryBuilder and Entity CRUD on SQLite |
| API | REST controller with #[Get], #[Post], #[Patch], #[Delete] and an in-page client |
| Internals | build cache, registered components, request facts |
To try the production build:
cd demo php ../sophia build # then set DEBUG=false; `php ../sophia clear` to go back php benchmark.php # compares debug and production timings
Publishing to Packagist
- Push this repository to GitHub under
giovanni-venturelli/sophia. - Create a version tag, e.g.:
git tag v0.1.0 git push origin v0.1.0
- Submit the repository URL to Packagist and set up the GitHub Service Hook so Packagist auto-updates on new tags.
After publish, consumers can composer require giovanni-venturelli/sophia.
Forms — end-to-end example
Route (already present in routes.php):
[ 'path' => 'forms/submit/:token', 'callback' => [\Sophia\Form\FormController::class, 'handle'], 'name' => 'forms.submit' ],
Template:
<form method="post" action="<?= $e($form_action('send')) ?>"> <?= $csrf_field() ?> <!-- fields --> </form>
The $form_action('send') helper generates a URL like /sophia/forms/submit/<token>. Make sure basePath is set in index.php.
Named routes — quick tip
Always add name to routes you want to link to from templates:
[ 'path' => 'about', 'component' => App\Pages\About\AboutLayoutComponent::class, 'name' => 'about', 'children' => include __DIR__ . '/pages/About/routes.php' ]
Then in templates:
<a href="<?= $e($url('about')) ?>">About</a>