giovanni-venturelli / sophia
Sophia is a lightweight component-based PHP framework with native PHP templates.
Installs: 45
Dependents: 0
Suggesters: 0
Security: 0
Stars: 1
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/giovanni-venturelli/sophia
Requires
- php: >=8.1
- ext-pdo: *
- ext-pdo_mysql: *
- ext-pdo_sqlite: *
- vlucas/phpdotenv: ^5.6
- dev-main
- 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
This package is auto-updated.
Last update: 2026-02-01 21:11:23 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)
- 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:
core/component(attributes, registry, renderer) - DI (Injector):
core/injector(root singletons + per‑component scoped providers) - Router:
core/router(maps requests to components or callbacks) - Database (optional):
core/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 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
│ └─ database/ # Optional DB service + ORM
├─ pages/ # Your component classes and PHP templates
├─ routes.php # Route table
├─ index.php # App bootstrap
└─ 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', '', '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('/sophia'); // 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 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.
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 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
/sophia/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.
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 the base path 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>