PHP code example of simcript / pano

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

return [
    'pano' => env('APP_ENV', 'production') === 'local' ? DefaultModule::class : null,
    ''     => \Modules\Default\DefaultModule::class,
];



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());
    }
}

$this->viewPath();   // .../Modules/Blog/Views
$this->filePath();   // .../Modules/Blog/Files
$this->logPath();    // .../Modules/Blog/Logs
$this->path();       // .../Modules/Blog
$this->name();       // "BlogModule" (short class name)

return [
    ''   => \Modules\Default\DefaultModule::class,
    'blog' => \Modules\Blog\BlogModule::class,
];

$router->get('/path', Handler::class, 'action');
$router->post('/path', Handler::class, 'action');
$router->put('/path', Handler::class, 'action');
$router->delete('/path', Handler::class, 'action');

// Route
$router->get('/users/[id]/posts/[postId]', UserHandler::class, 'post');

// Handler — argument order matches the route declaration
public function post($id, $postId): Response { ... }

$router->get('/files/[path*]', FileHandler::class, 'show');



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->get('name', $default);
$bag->set('key', $value);   // returns $this (mutable)
$bag->has('key');
$bag->remove('key');        // returns $this
$bag->all();

$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->router->post('/posts', PostHandler::class, 'store', [AuthInterceptor::class]);

$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');

return Response::html(
    $this->module->view()
        ->with(['post' => $post])
        ->layout('layout')
        ->render('post/show')
);

<!DOCTYPE html>
<html>
<head><title><?= $this->section('title', 'Pano') 

 $this->start('title') 

$this->module->log()->info('Post created', ['id' => $post['id']]);
$this->module->log()->error('DB connection failed', ['host' => $host]);
$this->module->log()->warning('Slow query', ['ms' => 1200]);

$router->command('app:info', \Modules\Default\Commands\DefaultCommand::class);
$router->command('blog:publish', \Modules\Blog\Commands\PublishCommand::class);



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>';
    }
}

env('APP_NAME', 'Pano');
config('app.debug', false);
url('posts/42');
dd($user, $request->getData());



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));
    }
}
bash
> MSYS_NO_PATHCONV=1 php pano / app:info
> 
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;