giovanni-venturelli / sophia
Sophia is a lightweight component-based PHP framework with Twig.
Installs: 7
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/giovanni-venturelli/sophia
Requires
- php: >=8.1
- ext-pdo: *
- ext-pdo_mysql: *
- ext-pdo_sqlite: *
- twig/twig: ^3.22
- vlucas/phpdotenv: ^5.6
README
This repository contains a minimal, production‑oriented PHP framework that blends:
- Component-based rendering with Twig
- A tiny, Angular‑inspired Dependency Injection system
- A simple but capable Router (components, API callbacks, parameters, guards)
- Optional database layer with a fluent QueryBuilder and Active Record‑style ORM
It is designed to be clear, explicit, and easy to extend. Components are plain PHP classes (annotated), services are auto‑wired, and routes map requests to either 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)
- Database integration (optional)
- Troubleshooting
- Deep dives (module READMEs)
- Including other READMEs by reference
What you get (features)
- Components rendered with Twig, with strict templates and a small set of helpers
- Property injection in components and constructor injection in services
- Root singletons via
#[Injectable(providedIn: 'root')] - Route configuration with parameters, named routes, redirects, nested routes, and simple guards
- Optional database service with fluent QueryBuilder and Active Record‑style entities
Architecture at a glance
- Components:
core/component(attributes, registry, renderer) - DI (Injector):
core/injector(root singletons + per‑component scoped providers) - Router:
core/router(maps requests to components or callbacks; integrates with renderer) - Database (optional):
core/database(connection service + ORM)
Flow for a component route:
Routermatches the incoming path → selects a component class.ComponentRegistrylazily registers it andRenderercreates aComponentProxy.ComponentProxyopens a DI scope, warms providers, creates the component, runs property injection, thenonInit().Renderercollects public properties/zero‑arg getters and renders the Twig template.
Installation
- PHP 8.1+
- Composer
composer install
If you use environment variables, add a .env file (Dotenv is included in composer.json) and configure the database as needed (see Database integration).
Project structure
.
├─ core/
│ ├─ component/ # Component system (attributes, registry, renderer)
│ ├─ injector/ # DI container + attributes
│ ├─ router/ # Router + middleware interface
│ └─ database/ # Optional DB service + ORM
├─ pages/ # Your component classes and Twig templates
├─ routes.php # Route table
├─ index.php # App bootstrap (renderer + router wiring)
└─ vendor/ # Composer dependencies
Quick start (index.php + routes.php)
Minimal bootstrap in index.php:
<?php use Sophia\Component\ComponentRegistry; use Sophia\Component\Renderer; use Sophia\Database\ConnectionService; use Sophia\Injector\Injector; use Sophia\Router\Router; require __DIR__ . '/vendor/autoload.php'; // Optional env (if your app uses Dotenv in your project) // $dotenv = Dotenv\Dotenv::createImmutable(__DIR__); // $dotenv->load(); // Optional DB (root singleton) $dbConfig = file_exists('config/database.php') ? require 'config/database.php' : ['driver' => 'sqlite', 'credentials' => ['database' => 'database/app.db']]; $db = Injector::inject(ConnectionService::class); $db->configure($dbConfig); $registry = ComponentRegistry::getInstance(); /** @var Renderer $renderer */ $renderer = Injector::inject(Renderer::class); $renderer->setRegistry($registry); $renderer->configure(__DIR__ . '/pages', __DIR__ . '/cache/twig', 'it', true); $renderer->addGlobalStyle('css/style.css'); $renderer->addGlobalScripts('js/scripts.js'); /** @var Router $router */ $router = Injector::inject(Router::class); $router->setComponentRegistry($registry); $router->setRenderer($renderer); $router->setBasePath('/test-route'); // optional, if app lives in a subfolder require __DIR__ . '/routes.php'; $router->dispatch();
Define routes in routes.php:
<?php use App\Pages\Home\HomeComponent; use Sophia\Router\Router; $router = Router::getInstance(); $router->configure([ [ 'path' => 'home/:id', // URL with param 'component' => HomeComponent::class, // Component class 'name' => 'home', // Named route 'data' => [ 'title' => 'Home Page' ], // Route-scoped data ], ]);
Create your first component
Component class under pages/... and a Twig template next to it (or inside the configured pages path):
<?php namespace App\Pages\Home; use Sophia\Component\Component; #[Component(selector: 'app-home', template: 'home.html.twig')] class HomeComponent { public string $title = 'Welcome'; public string $id; public function onInit(): void { // Optionally compute public state for the template } }
Template home.html.twig:
<h1>{{ title }}</h1> <p>User ID: {{ id }}</p>
The renderer will pass route params (:id) as initial data to the root component, so id will be available.
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.html.twig', providers: [UserService::class])] class UsersComponent { #[Inject] private UserService $users; // resolved from providers 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; they are available to components and templates. - Name routes with
nameand generate URLs from PHP or Twig using theurl()helper. - Provide
dataon routes; read them in Twig withroute_data().
Twig helpers from the renderer:
<a href="{{ url('home', { id: 123 }) }}">Go home</a> <p>Title: {{ route_data('title') }}</p>
More details: Router.
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 Twig variable: templates run with strict variables; expose data via public properties or zero‑arg getters.
- Injection error: add
#[Inject]to typed component properties; mark services as#[Injectable](root when needed) 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
Using this repository as a package + demo
This repo is organized so that the core framework (package) is published to Packagist, while the demo app stays in-repo only.
- Package (library):
giovanni-venturelli/sophia(root of this repo)- Namespaces exported:
Sophia\\*(component, injector, router, form, database) - Packagist dist excludes the demo and app assets via
.gitattributes
- Namespaces exported:
- Demo app:
/demo(not included in the Packagist dist)- Depends on the package via Composer repository of type
pathto the repo root - Autoloads the demo namespaces from the project folders (
../pages,../Shared,../services)
- Depends on the package via Composer repository of type
Install the package (as a dependency) in another project
composer require giovanni-venturelli/sophia
Run the demo locally from this repo
- Install dependencies for the demo (uses a path repo to the root library):
cd demo
composer install
- Start a PHP dev server pointing to the demo folder (or your web server root to
demo/):
php -S localhost:8080 -t demo
Then open:
- http://localhost:8080/index.php/home/123 (adjust paths if needed)
Notes
- The demo reuses the project folders
pages/,Shared/,services/,config/,css/,js/,cache/from the repo root. - The router base path in the demo is set to
/test-route/demo. If you serve it at a different path, update$basePathindemo/index.phpand the global asset paths. - The package requires PHP >= 8.1; the demo also requires
vlucas/phpdotenvfor.envloading.
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.
What's new (highlights)
- Router and Renderer are now injectable root services (
#[Injectable(providedIn: 'root')]), obtainable viaInjector::inject(...). Renderer::configure()lets you (re)initialize templates path, cache, language, and debug at runtime.- Global assets APIs on the renderer:
addGlobalStyle(),addGlobalScripts(),addGlobalMetaTags(). - Rich Twig helper set:
component,slot,url,route_data,form_action,csrf_field,flash/peek_flash/has_flash,form_errors,old,set_title,add_meta. - Base path is a single source of truth: set it only once in the bootstrap (
index.phpordemo/index.php).
Forms — end-to-end example
Route (already present in routes.php):
[ 'path' => 'forms/submit/:token', 'callback' => [\Sophia\Form\FormController::class, 'handle'], 'name' => 'forms.submit' ],
Twig template:
<form method="post" action="{{ form_action('send') }}"> {{ csrf_field()|raw }} <!-- fields --> </form>
The form_action('send') helper generates a URL like /test-route/forms/submit/<token> (or /test-route/demo/... in the demo). Make sure the base path is set in index.php and not in routes.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 Twig:
<a href="{{ url('about') }}">About</a>