Search by

Sophia is a lightweight component-based PHP framework with native PHP templates.

Package info

github.com/giovanni-venturelli/sophia

pkg:composer/giovanni-venturelli/sophia

Statistics

Installs: 66

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 0

v0.2.0 2026-10-20 21:48 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)

  • 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:

  1. Router matches the incoming path → selects a component class
  2. ComponentRegistry lazily registers it and Renderer creates a ComponentProxy
  3. ComponentProxy opens a DI scope, warms providers, creates the component, runs property injection, then onInit()
  4. Renderer collects 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() and addGlobalStyle() no longer matters. Pass paths relative to the app (css/style.css), not prefixed with the base path.
  • DEBUG=false in .env now 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/:id to capture params; available to components and templates
  • Name routes with name and generate URLs using the url() helper
  • Provide data on routes; read them with route_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 providers in 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 in providers
  • Routing mismatch: check basePath, path normalization, and that names/params match when calling url()

Deep dives (module READMEs)

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 namespace Sophia\ (component, injector, router, form, database)
    • The demo/ folder, lint configuration and composer.lock are excluded from the dist via .gitattributes
  • Demo app: demo/, a showcase of every feature, each shown live next to its own source code
    • Its namespace App\ (mapped to demo/app/) is declared in autoload-dev of the root composer.json, so it is never registered in projects that install the package

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

  1. Push this repository to GitHub under giovanni-venturelli/sophia.
  2. Create a version tag, e.g.:
git tag v0.1.0
git push origin v0.1.0
  1. 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>