pano-php / pano
Pano application skeleton
Requires
- php: >=8.2
- pano-php/framework: ^1.7
Requires (Dev)
- phpunit/phpunit: ^12.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
A minimal, ready-to-run application skeleton built on top of the Pano nano-framework.
Pano is a lightweight PHP runtime that gives you an explicit, predictable
foundation with full architectural control. This skeleton wires up that
runtime with a sensible project layout, a working Default module, an example
package, configuration, and web + CLI entry points so you can start building
your own domains immediately.
Built for Pano Framework
^1.7.
The framework itself is distributed as a pure library — this skeleton provides the application scaffolding (public/,config/,src/, CLI, …).
Requirements
- PHP >= 8.2
- Composer
Installation
Create a new project with Composer:
composer create-project pano-php/pano my-app
cd my-app
Or clone this repository and install dependencies manually:
git clone https://github.com/pano-php/pano.git my-app
cd my-app
composer install
The .env file is created automatically from .env.example after install.
If it isn't, copy it yourself:
cp .env.example .env
Quick Start
Start the built-in PHP development server:
php -S localhost:8000 -t public
Open http://localhost:8000 — you should see the welcome page from the
Default module.
CLI example (root module + app:info command from InfoPackage):
php pano / app:info
Project Structure
my-app/
├── pano # CLI entry point (executable)
├── public/
│ ├── index.php # Web front controller
│ ├── error.php # HTML error template
│ └── .htaccess # Apache rewrite rules
├── config/
│ └── app.php # Application configuration
├── src/
│ ├── Foundation/
│ │ ├── DefaultFoundation.php # Module registry + exception binding
│ │ └── DefaultException.php # HTML errors via public/error.php
│ ├── Modules/
│ │ └── Default/
│ │ ├── DefaultModule.php # setup(), view(), log(); imports InfoPackage
│ │ ├── Handlers/
│ │ ├── Interceptors/
│ │ └── Views/
│ └── Packages/
│ └── Info/
│ ├── InfoPackage.php # BasePackage — registers app:info
│ └── Commands/
│ └── InfoCommand.php
├── tests/
├── .env
├── .env.example
├── composer.json
└── phpunit.xml
Namespace root: Src\ (PSR-4 via composer.json).
Entry points
Both entry points pass the project base path and a custom Foundation into Boot:
// public/index.php (new \Pano\Foundation\Boot($basePath, new \Src\Foundation\DefaultFoundation())) ->run($_SERVER); // pano (CLI) (new \Pano\Foundation\Boot($basePath, new \Src\Foundation\DefaultFoundation())) ->run($argv);
| Value | Meaning |
|---|---|
PANO_STARTED |
Request start timestamp (microtime) |
$basePath / BASE_PATH |
Project root (set by Boot) |
FOUNDATION |
Active Foundation instance (set by Boot) |
config() |
Values from config/*.php loaded at bootstrap |
Configuration
.env
APP_NAME=Pano APP_ENV=local APP_KEY=base64:your-key-here APP_DEBUG=true APP_URL=https://example.test
config/app.php
<?php return [ 'name' => env('APP_NAME', 'Pano'), 'env' => env('APP_ENV', 'local'), 'key' => env('APP_KEY', null), 'debug' => env('APP_DEBUG', false), 'url' => env('APP_URL', null), ];
Access values with config('app.name'), env('APP_DEBUG'), etc.
Module registry
Modules are not listed in a config file. They are registered on the Foundation:
// src/Foundation/DefaultFoundation.php namespace Src\Foundation; use Pano\Foundation\Foundation; use Pano\Kernel\ModuleResolverEnum; use Src\Modules\Default\DefaultModule; class DefaultFoundation extends Foundation { protected static array $modules = [ '' => DefaultModule::class, // root module (PATH) // 'blog' => [ // 'class' => \Src\Modules\Blog\BlogModule::class, // 'resolver' => ModuleResolverEnum::PATH, // ], ]; public static function exception(): string { return DefaultException::class; } }
Add more keys as you add modules. Resolvers: PATH (default), SUBDOMAIN, HOST, QUERY, HEADER.
Core Concepts (in this skeleton)
| Concept | Role |
|---|---|
| Foundation | Module map + which concrete classes implement request/response/router/… |
| Module | Domain unit: setup(), view(), log(); may import packages |
| Package | BasePackage attached to a module; shares the router; cannot import packages |
| Handler | Action that returns a Response |
| Interceptor | Runs before/after the handler on a route |
| Command | CLI action returning ResultCodeEnum |
Default resolution is PATH (first URL segment = module key); empty key '' serves /. Other strategies: SUBDOMAIN, HOST, QUERY, HEADER.
The Default Module
final readonly class DefaultModule extends BaseModule { public function __construct(BaseRequest $request, BaseFoundation $foundation) { parent::__construct( request: $request, foundation: $foundation, packages: [InfoPackage::class] ); } public function setup(): void { $this->router->get('/', DefaultHandler::class, 'info', [DefaultInterceptor::class]); } public function view(): BaseView { return new View($this->viewPath()); } public function log(): BaseLogger { return new Logger($this->logPath()); } }
- Routes and commands are registered in
setup()(the router is injected by Boot beforesetup()runs). - Packages listed in the constructor are imported via
importPackages()beforesetup().
Packages
InfoPackage registers the CLI command app:info:
php pano / app:info
Packages live under src/Packages/, extend BasePackage, and implement setup(), view(), and log() like modules.
Building a Module
- Create
src/Modules/Blog/BlogModule.phpextendingBaseModule. - Implement
setup(),view(), andlog(). - Register the class on
DefaultFoundation::$modules. - Add handlers (extend
BaseHandler, returnResponse), optional interceptors and views. - Optionally attach packages via the constructor
packagesargument. - Test with
./vendor/bin/phpunit.
Example registration:
protected static array $modules = [ '' => DefaultModule::class, 'blog' => \Src\Modules\Blog\BlogModule::class, ];
Example routes inside setup():
public function setup(): void { $this->router->get('/', PostHandler::class, 'index'); $this->router->get('/posts/[id]', PostHandler::class, 'show'); $this->router->post('/posts', PostHandler::class, 'store', [AuthInterceptor::class]); $this->router->command('blog:publish', PublishCommand::class); $this->router->group('/admin', function ($router) { $router->get('/dashboard', DashboardHandler::class, 'index'); }, [AuthInterceptor::class]); }
CLI
php pano <module-path> <command> [args...] [--options...]
| Example | Meaning |
|---|---|
php pano / app:info |
Root module (''), command app:info |
php pano blog blog:publish 42 |
Module blog, command blog:publish |
On Windows / Git-Bash, use
MSYS_NO_PATHCONV=1 php pano / app:infoif/is mangled.
Testing
./vendor/bin/phpunit
phpunit.xml defines the Pano Test Suite under tests/. Autoload-dev maps Tests\ → tests/.
Web Server Setup
Development
php -S localhost:8000 -t public
Apache
Point DocumentRoot to public/. The included .htaccess removes trailing slashes, serves real files, forwards other requests to index.php, and preserves the Authorization header.
Nginx
server { listen 80; server_name your-domain.test; root /var/www/my-app/public; index index.php; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { fastcgi_pass unix:/var/run/php/php8.2-fpm.sock; fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } }
Learn More
- Skeleton developer guide: DOCUMENTATION.md
- Framework source & docs: pano-php/framework
- Framework API: DOCUMENTATION.md
- Architecture: ARCHITECTURE.md
- Philosophy: MANIFESTO.md
Pano is deliberately unopinionated — you bring the architecture. The framework should never make decisions on your behalf.
License
The MIT License (MIT). See LICENSE.