Download the PHP package simcript/pano without Composer
On this page you can find all versions of the php package simcript/pano. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download simcript/pano
More information about simcript/pano
Files in simcript/pano
Informations about the package pano
Pano Application Skeleton
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, configuration,
and a web + CLI entry point so you can start building your own domains
immediately.
Built for Pano Framework
^1.4(currentlyv1.4.4).
Requirements
- PHP >= 8.2
- Composer
Installation
Create a new project with Composer:
Or clone this repository and install dependencies manually:
The .env file is created automatically from .env.example after install.
If it isn't, copy it yourself:
Quick Start
Start the built-in PHP development server:
Open http://localhost:8000 in your browser — you should see the "It works!"
welcome page. The skeleton is alive.
Project Structure
Two constants are defined at the very start of every entry point and drive the whole runtime:
| Constant | Meaning |
|---|---|
PANO_STARTED |
Request start timestamp (microtime), for timing |
BASE_PATH |
Absolute path to the project root, with trailing / |
Everything — config loading, .env resolution, module paths — is relative to
BASE_PATH, so keep that in mind if you move entry points.
Configuration
.env
Tip: Pano parses
.envvalues.true/false/nullbecome proper booleans/null, and numeric strings become numbers.
config/app.php
Reads environment values into a config array. Access any value anywhere using
the config() helper:
config/modules.php
Maps a module key to a module class. This is how Pano decides which module handles the current request:
- The empty key
''is the default module. 'pano'is only active in thelocalenvironment.
The Module Resolver
Pano routes an incoming request to a module before it routes to a handler.
How the module is chosen depends on MODULE_RESOLVER:
path (default)
The first URL segment is the module key. The remainder is the route path.
| URL | Module key | Route |
|---|---|---|
/blog/posts/12 |
blog |
/posts/12 |
/ |
'' |
/ |
subdomain
The subdomain is the module key. The root domain is derived from APP_URL,
so only the leading sub-part of that host is treated as a module:
| Host | Module key |
|---|---|
blog.neda.tst (APP_URL=https://neda.tst) |
blog |
api.v2.neda.tst |
api.v2 |
neda.tst (the root itself) |
'' |
If the resolver is neither path nor subdomain, an Exception is thrown.
If no module matches the resolved key, Pano throws
"No module found for '<name>'". Make sure every reachable key is registered inconfig/modules.php.
Core Concepts
Pano is intentionally small. Five concepts carry the whole runtime:
| Concept | Responsibility |
|---|---|
| Module | A self-contained domain; defines routes, views, logging |
| Handler | A controller-like class that produces a Response |
| Interceptor | Runs before handlers (onRequest) and after (onResponse) |
| Command | A CLI action, like a console controller |
| View | Renders templates with layouts and sections |
The base contracts live in the Pano\Kernel namespace; ready-to-use concrete
implementations live in Pano\Foundation.
| Concept | Base (abstract) contract | Concrete implementation |
|---|---|---|
| Module | Pano\Kernel\BaseModule |
(you extend it) |
| Router | Pano\Kernel\BaseRouter |
Pano\Foundation\Router |
| Request | Pano\Kernel\BaseRequest |
Pano\Foundation\Request |
| Response | Pano\Kernel\BaseResponse |
Pano\Foundation\Response |
| View | Pano\Kernel\BaseView |
Pano\Foundation\View |
| Handler | Pano\Kernel\BaseHandler |
(you extend it) |
| Interceptor | Pano\Kernel\BaseInterceptor |
(you extend it) |
| Command | Pano\Kernel\BaseCommand |
(you extend it) |
| Logger | Pano\Kernel\BaseLogger |
Pano\Foundation\Logger |
| Exception | Pano\Kernel\BaseException |
Pano\Foundation\Exception |
Important: Always extend the
Pano\Kernel\Base*contracts (and use thePano\Foundation\*implementations). The olderPano\Core/Pano\Enumnamespaces no longer exist.
Building a Module
A module is a readonly class extending BaseModule. It must define three
methods: routes(), view(), and log().
BaseModule gives you convenient path helpers, all resolved relative to the
module's own directory via reflection:
Register the module in config/modules.php:
Now requests to /blog/... are handled by BlogModule.
Routing
Routes are registered inside the module's routes() method on the injected
$router. Supported HTTP verbs:
Route Parameters
Parameters are declared with brackets and are passed as method arguments in declaration order:
Parameter flags:
| Syntax | Meaning |
|---|---|
[id] |
Required segment |
[id?] |
Optional (must be the last segment) |
[id*] |
Catch-all (must be the last segment) |
Optional and catch-all parameters must always be the last route segment.
Handlers
A handler is a class extending BaseHandler. Each action is a public method
that returns a Response. The return type must be declared and must be
BaseResponse (or a subclass) — Pano enforces this.
The handler receives the request and its module via constructor injection:
BaseHandler exposes:
Method override
HTML forms can only GET/POST. To submit PUT/DELETE/PATCH, Pano
honors a method override on POST requests via either:
- the
X-HTTP-Method-Overrideheader, or - a
_methodfield in the POST body.
The resolved verb becomes the route's HTTP method automatically.
Responses
Pano\Foundation\Response offers expressive factory methods:
Every method accepts an optional HttpStatusEnum and a headers array. The JSON
helper encodes with JSON_UNESCAPED_UNICODE so Persian/Arabic text is preserved.
Fluent mutation
Every mutator returns $this, so you can chain after construction:
Useful HttpStatusEnum values: OK, CREATED, NO_CONTENT, MOVED_PERMANENTLY,
FOUND, BAD_REQUEST, UNAUTHORIZED, FORBIDDEN, NOT_FOUND,
UNPROCESSABLE_ENTITY, INTERNAL_SERVER_ERROR, … (all standard HTTP codes).
Automatic error rendering
If a handler (or anything in the pipeline) throws, Response::exception($e, $request)
turns the throwable into an appropriate response:
| Context | Output |
|---|---|
| CLI request | colored terminal line, ResultCodeEnum::ERROR |
expectsJson() |
$e->toArray($debug) as JSON |
| otherwise | $e->toHtml($debug) as HTML |
This is wired into the global handler, so you never need to wrap your handlers in try/catch for rendering.
Sending
Calling send() writes the HTTP status line, headers, and body to the output
stream. It is idempotent — a response can only be sent once. The router
calls send() for you; you normally just return the response.
The Bag
Bag (Pano\Foundation\Bag, extending Pano\Kernel\BaseBag) is Pano's
lightweight, chainable key/value container. You already met it as
$request->attributes. You can also use it anywhere you need structured data.
It behaves like an array — it implements ArrayAccess, IteratorAggregate,
and Countable:
Basic operations
Functional helpers
Most helpers return a new Bag (immutable style), so they chain:
Deep search
Bags can be nested (arrays and other Bags inside). Pano finds values or keys anywhere in the tree and returns their dot-paths:
Paths use . as the separator (e.g. user.address.city), making Bag handy
for config trees, JSON payloads, and nested request data.
Interceptors
Interceptors are cross-cutting filters. They run before the handler
(onRequest) and after it returns (onResponse), in registration order.
Attach interceptors to a route as the fourth argument:
Execution order
For a route registered with [A::class, B::class]:
So onRequest runs in registration order and onResponse runs in
reverse order — exactly like layered middleware (Russian-doll model).
Sharing state with the handler
The same $request instance is shared across all interceptors and the
handler, so data left on $request->attributes in onRequest() is visible in
the handler. This is the recommended way to pass the authenticated user, a
request ID, etc. (See The Bag.)
Requests
The current request is available on handlers and interceptors as $this->request
(Pano\Foundation\Request). Key accessors:
Body parsing (getData())
getData() decodes the request body based on the Content-Type:
| Content-Type | getData() returns |
|---|---|
| (form submitted) | $_POST |
application/json |
decoded JSON array |
application/x-www-form-urlencoded |
parse_str array |
| other / empty | [] (empty array) |
File uploads (getFiles())
getFiles() returns $_FILES normalized. Multi-file inputs
(e.g. <input name="photos[]">) are restructured into an indexed list, so you
always iterate a flat array:
Headers
Keys are lowercased, so read them in lowercase regardless of how the client sent them:
Sharing state — $request->attributes
The request carries a mutable Bag named attributes. It is the idiomatic
channel for passing data from an interceptor to the handler — the
authenticated user, a request ID, feature flags, etc. (See The Bag.)
Views & Templating
A module renders templates from its Views/ directory using
Pano\Foundation\View. Templates are plain PHP, with layout + section support.
Render a view with data:
Views/layout.php — the wrapper:
Views/post/show.php — a page that fills the layout's sections:
Template helpers available as $this inside views:
| Method | Description |
|---|---|
$this->start('name') / $this->end() |
Open/close a named section |
$this->section('name', 'default') |
Echo a section's content (with fallback) |
$this->fragment('partials/card', $data) |
Include a sub-template (with extra data) |
$this->e($value) |
HTML-escape a stringable value |
Always escape untrusted output with
$this->e().
Logging
Each module owns its logs. Create the logger via $this->log() and call any
PSR-style level method:
Available levels: emergency, alert, critical, error, warning,
notice, info, debug.
By default the framework's Logger writes to a daily file under the module's
Logs/ directory (log-YYYY-MM-DD.log).
CLI Commands
Pano has a single CLI entry point: the pano executable at the project root.
Invocation format
The first positional argument is the module path (matching a key in
config/modules.php), the second is the command name.
Examples for the skeleton's Default module:
Windows note: The
/used for the root module can be mangled bycmd.exe/ Git Bash path conversion. Prefix the command withMSYS_NO_PATHCONV=1when invoking from Git Bash, or use a named module key instead:
Registering a command
Inside a module's routes(), call command() with a command name and a
command class:
The command class must extend BaseCommand and implement handle():
Inside a command
BaseCommand gives you:
The $arguments array received by handle() is exactly getPositional()
(i.e. everything after the command that does not start with --).
Return codes
handle() returns a ResultCodeEnum:
| Value | Meaning |
|---|---|
OK |
success |
ERROR |
general failure |
INVALID |
invalid input / usage error |
This drives the terminal output color and signals failure to the shell.
Exceptions & Errors
Throw Pano\Foundation\Exception to control the HTTP response status, message,
and optional payload. Pano formats it automatically based on the request:
How it renders
| Context | Output |
|---|---|
| CLI request | colored terminal line |
expectsJson() |
JSON body { "message": ..., "data": ... } |
| otherwise | HTML error page |
In APP_DEBUG=true mode, the rendered body includes the exception class name and
stack trace; in production it is hidden.
Custom exception types
For richer domain errors, extend BaseException and implement toArray() and
toHtml():
Throw it anywhere in a handler or interceptor — the global handler will render it correctly.
Non-BaseException throwables
Plain \Throwable instances (PHP errors, third-party exceptions) are rendered as
a generic 500 Server Error, with the real message shown only in debug mode.
Helper Functions
These globals are always available (autoloaded by the framework):
| Function | Description |
|---|---|
env($key, $default = null) |
Read a value from .env |
config($key, $default = null) |
Dot-notation config read (e.g. config('app.name')) |
url($path) |
Build an absolute URL using APP_URL |
currentUrl() |
The current request's absolute URL |
dd(...$args) |
Dump-and-die debug helper (CLI or HTML aware) |
Testing
The skeleton ships with PHPUnit. Tests live in tests/ under the Tests\
namespace.
A starter test is included at tests/DefaultModuleTest.php. Example:
phpunit.xml is preconfigured with the Pano Test Suite.
Web Server Setup
Apache
public/.htaccess is already configured. Point your virtual host DocumentRoot
to the public/ directory. It handles:
- Removing trailing slashes
- Serving real files/directories directly
- Forwarding everything else to
public/index.php - Preserving the
Authorizationheader (for JWT / Bearer tokens)
Nginx
Development server
Build Your First Feature — Checklist
- Create a module under
modules/<Name>/<Name>Module.phpextendingPano\Kernel\BaseModule. - Register it in
config/modules.phpunder a resolver key. - Add handlers (extending
Pano\Kernel\BaseHandler) returning aPano\Foundation\Response. - Define routes in the module's
routes()method. - (Optional) Add interceptors for auth/validation, commands for CLI tasks, and views for HTML.
- Test it with
./vendor/bin/phpunit.
Learn More
- Framework source & docs: simcript/pano-framework
- Architecture:
ARCHITECTURE.mdin the framework repo - Philosophy:
MANIFESTO.mdin the framework repo
Pano is deliberately unopinionated — you bring the architecture. The framework should never make decisions on your behalf.
License
The MIT License (MIT). See LICENSE.