1. Go to this page and download the library: Download simcript/pano library. Choose the download type require.
2. Extract the ZIP file and open the index.php.
3. Add this code to the index.php.
<?php
require_once('vendor/autoload.php');
/* Start to develop here. Best regards https://php-download.com/ */
simcript / pano example snippets
config('app.name'); // "Pano"
config('app.debug', false); // true, with a fallback
namespace Modules\Blog;
use Pano\Foundation\Exception;
use Pano\Foundation\Logger;
use Pano\Foundation\View;
use Pano\Kernel\BaseLogger;
use Pano\Kernel\BaseModule;
use Pano\Kernel\BaseRouter;
use Pano\Kernel\BaseView;
use Modules\Blog\Handlers\PostHandler;
use Modules\Blog\Interceptors\AuthInterceptor;
final readonly class BlogModule extends BaseModule
{
public function routes(): BaseRouter
{
$router = new Router($this->request, $this);
// HTTP routes: $router->METHOD(path, Handler::class, action, [interceptors])
$router->get('/posts', PostHandler::class, 'index');
$router->get('/posts/[id]', PostHandler::class, 'show');
$router->post('/posts', PostHandler::class, 'store', [AuthInterceptor::class]);
// CLI commands
$router->command('blog:publish', \Modules\Blog\Commands\PublishCommand::class);
return $router;
}
public function view(): BaseView
{
return new View($this->viewPath());
}
public function log(): BaseLogger
{
return new Logger($this->logPath());
}
}
namespace Modules\Blog\Handlers;
use Pano\Foundation\Response;
use Pano\Kernel\BaseHandler;
use Pano\Kernel\HttpStatusEnum;
final class PostHandler extends BaseHandler
{
public function index(): Response
{
return Response::json([
'posts' => ['Pano 101', 'Routing in depth'],
]);
}
public function show($id): Response
{
return Response::json(['id' => $id, 'title' => 'Hello Pano']);
}
public function store(): Response
{
// getData() auto-parses the body based on Content-Type:
// $_POST → application/json → application/x-www-form-urlencoded
$data = $this->request->getData();
// ...persist...
return Response::json(['created' => true], HttpStatusEnum::CREATED);
}
}
$this->request; // the current Request
$this->module; // the owning Module
Response::html($htmlString); // text/html
Response::json($arrayOrObject); // application/json
Response::text($plain); // text/plain
Response::redirect($url); // 302 redirect
Response::stream(fn() => readfile($path), 'application/pdf'); // streamed body
Response::terminal('Done'); // CLI: green text
Response::terminal('Failed', ResultCodeEnum::ERROR); // CLI: red text
Response::make($body, HttpStatusEnum::CREATED, ['X-Foo' => 'bar']); // custom
return Response::json($data)
->setStatus(HttpStatusEnum::CREATED)
->setHeader('X-Request-Id', $id)
->setHeaders(['Cache-Control' => 'no-store']);
// Or override the body after creation
return Response::make(null)
->setStatus(HttpStatusEnum::NO_CONTENT);
use Pano\Foundation\Bag;
$bag = new Bag(['name' => 'Pano', 'tags' => ['php', 'web']]);
$bag['name']; // get
$bag['name'] = 'X'; // set
isset($bag['name']); // has
count($bag);
foreach ($bag as $k => $v) { ... }
$bag->merge($otherBagOrArray); // union by key
$bag->replace($otherBagOrArray); // array_replace semantics
$bag->only(['name', 'email']); // keep only these keys
$bag->except(['password']); // drop these keys
$bag->map(fn($v, $k) => strtoupper($v));
$bag->filter(fn($v, $k) => $v !== null);
$bag->find('php'); // 'tags.0' — first path to the VALUE
$bag->findAll('php'); // ['tags.0'] — all paths to the value
$bag->findKey('tags'); // 'tags' — first path to the KEY
$bag->findAllKeys('tags'); // ['tags'] — all paths to the key
namespace Modules\Blog\Interceptors;
use Pano\Kernel\BaseInterceptor;
use Pano\Kernel\BaseResponse;
use Pano\Foundation\Exception;
use Pano\Foundation\Response;
use Pano\Kernel\HttpStatusEnum;
class AuthInterceptor extends BaseInterceptor
{
public function onRequest(): void
{
// headers are LOWERCASED
$token = $this->request->getHeaders()['authorization'] ?? '';
if (!str_starts_with($token, 'Bearer ')) {
throw new Exception(
'Unauthorized',
401,
HttpStatusEnum::UNAUTHORIZED
);
}
// pass data downstream to the handler via the shared attributes Bag
$this->request->attributes->set('userId', $this->resolve($token));
}
public function onResponse(BaseResponse $response): BaseResponse
{
// Add headers to every response handled by this interceptor
return $response->setHeader('X-Module', 'Blog');
}
}
$this->request->getMethod(); // HttpMethodEnum (honors _method / X-HTTP-Method-Override)
$this->request->getUrl(); // path part of the URL (without the module segment)
$this->request->getQueries(); // parsed query params (associative)
$this->request->getData(); // body, auto-parsed by Content-Type
$this->request->getHeaders(); // all headers — keys are LOWERCASED
$this->request->getFiles(); // $_FILES, normalized (multi-file flattened)
$this->request->getSegments(); // URL segments as array
$this->request->getHost(); // scheme + host
$this->request->expectsJson(); // true if Accept header asks for JSON
foreach ($this->request->getFiles()['photos'] ?? [] as $file) {
move_uploaded_file($file['tmp_name'], $target);
}
$this->request->getHeaders()['authorization']; // not 'Authorization'
// in an interceptor (runs before the handler)
$this->request->attributes->set('user', $user);
// in the handler
$user = $this->request->attributes->get('user');
namespace Modules\Blog\Commands;
use Pano\Kernel\BaseCommand;
use Pano\Kernel\ResultCodeEnum;
final class PublishCommand extends BaseCommand
{
public function handle(array $arguments): ResultCodeEnum
{
// Positional arguments: $arguments (indexed array)
$id = $arguments[0] ?? null;
// --options are available on the request
$dryRun = $this->request->getOptions()['dry-run'] ?? false;
if ($id === null) {
$this->error('Usage: blog:publish <id>');
return ResultCodeEnum::INVALID;
}
$this->info("Published post {$id}" . ($dryRun ? ' (dry-run)' : ''));
return ResultCodeEnum::OK;
}
}
$this->request; // the CLIRequest
$this->module; // the owning module (so $this->module->log() works in CLI too)
$this->info($text); // print a green line
$this->error($text); // print a red line
use Pano\Foundation\Exception;
use Pano\Kernel\HttpStatusEnum;
throw new Exception(
'Post not found',
code: 404,
status: HttpStatusEnum::NOT_FOUND,
payload: ['hint' => 'Check the post id']
);
namespace Modules\Blog\Exceptions;
use Pano\Kernel\BaseException;
use Pano\Kernel\HttpStatusEnum;
final class ValidationException extends BaseException
{
public function toArray(bool $debug = false): array
{
return [
'message' => $this->getMessage(),
'errors' => $this->payload ?? [],
];
}
public function toHtml(bool $debug = false): string
{
return '<h1>Validation failed</h1><pre>'
. htmlspecialchars($this->getMessage())
. '</pre>';
}
}
namespace Tests;
use PHPUnit\Framework\TestCase;
use Modules\Default\DefaultModule;
class DefaultModuleTest extends TestCase
{
public function test_module_class_exists()
{
$this->assertTrue(class_exists(DefaultModule::class));
}
}