Nano Framework PHP

v1.0.0 2025-01-26 02:33 UTC

This package is auto-updated.

Last update: 2025-01-26 21:39:46 UTC


README

Nano framework PHP para desenvolvimento de API's e aplicações Web

Requisitos

  • Composer
  • PHP >= 8.4.0
  • MySQL

Certifique-se de que as extensões abaixo, estejam habilitadas no php.ini

  • extension=pdo_mysql
  • extension=mbstring
  • extension=curl

Instalação

composer require jonathansilva/nano

Configuração

Apache

RewriteEngine On
Options All -Indexes

RewriteCond %{SCRIPT_FILENAME} !-f
RewriteCond %{SCRIPT_FILENAME} !-d
RewriteRule ^(.*)$ index.php?uri=/$1 [L,QSA]

Nginx

location / {
    if ($script_filename !~ "-f") {
        rewrite ^(.*)$ /index.php?uri=/$1 break;
    }
}

.env.example

DB_HOST=localhost
DB_USER=root
DB_PASS=password
DB_NAME=database

JWT_KEY=anything
JWT_EXP_IN_HOURS=8

COOKIE_EXP_IN_DAYS=1
COOKIE_DOMAIN=localhost
COOKIE_HTTPS=false
COOKIE_HTTPONLY=true
COOKIE_SAMESITE=Strict

CURL_SSL_VERIFYPEER=false

TEMPLATE_ENGINE_CACHE=false

Duplique o arquivo, renomeie para .env e altere os valores

.gitignore

.idea
.env
.vscode/
vendor/
cache/
composer.phar
composer.lock

index.php

<?php

require_once __DIR__ . '/vendor/autoload.php';

$app = Nano\Core\Router\Instance::create();

$app->use('App\Middleware\Token\Assert');

$app->notFound('App\Callback\Page\NotFound');

$app->get('/about', fn ($req, $res) => $res->view('about'));

$app->get('/hello/{name}', function ($req, $res) {
    echo $req->params()->name;
});

$app->post('/api/test', function ($req, $res) {
    $res->json(200, array('message' => 'It workerd!'));
});

$app->start();

Routes

Verbos: GET, POST, PUT, PATCH e DELETE

$app->get(
    '/me', // Path
    'App\Callback\Page\Me', // Callback
    ['App\Middleware\Token\Ensure'] // Middleware
);

O Callback/Controller não permite chamada de método ( exemplo: 'Namespace\Login@index' )

$app->get('/login', 'App\Callback\Page\Login');

Crie o método 'handle'

class Login
{
    public function handle($req, $res)
    {
        // TODO
    }
}

Routes file

Para carregar um arquivo de rotas, utilize o método 'load'

$app->load(__DIR__ . '/src/routes.xml');

routes.xml

<?xml version="1.0" encoding="UTF-8"?>

<routes>
    <route>
        <path>/</path>
        <method>GET</method>
        <callback>App\Callback\Page\Home</callback>
    </route>

    <route>
        <path>/me</path>
        <method>GET</method>
        <callback>App\Callback\Page\Me</callback>
        <middlewares>
            <middleware>App\Middleware\Token\Ensure</middleware>
        </middlewares>
    </route>

    <route>
        <path>/dashboard</path>
        <method>GET</method>
        <callback>App\Callback\Page\Dashboard</callback>
        <middlewares>
            <middleware>App\Middleware\Token\Ensure</middleware>
            <middleware>App\Middleware\Role::admin</middleware>
        </middlewares>
    </route>
</routes>

Middleware

Middlewares devem ser informados no terceiro parâmetro da rota ( Routes )

Para configurar um middleware global, utilize o método 'use'. É possível configurar quantos forem necessários

$app->use('App\Middleware\A');
$app->use('App\Middleware\B');

Veja abaixo alguns exemplos de middlewares

Assert Middleware

Middleware global que faz proteção contra CSRF e decodifica o payload do JWT

CSRF

Os formulários deverão ter um campo 'hidden' chamado 'csrf'

<input type="hidden" name="csrf" value="{{ $csrf }}">

O uso do CSRF necessita do session_start(); no index.php

JWT

Se o token existir mas for inválido:

( Web ) Redireciona para a página 'login'

( API ) Retorna 'Invalid or expired token'

Caso for válido, o payload será enviado para o próximo middleware ou controller, podendo ser recuperado usando $req->query() ( veja um exemplo em Role Middleware )

Se não existir, vai para o próximo middleware ou executa o controller

<?php

namespace App\Middleware\Token;

use Nano\Core\Security\{ CSRF, JWT };

class Assert
{
    public function handle($req, $res)
    {
        CSRF::assert($req, $res);
        JWT::assert($req, $res, '/login');
    }
}

Ensure Middleware

Será chamado em rotas onde a autenticação é obrigatória

Se não encontrar o token:

( Web ) Redireciona para a página 'login'

( API ) Retorna 'Authorization token not found in request'

<?php

namespace App\Middleware\Token;

use Nano\Core\Security\JWT;

class Ensure
{
    public function handle($req, $res)
    {
        JWT::ensure($req, $res, '/login');
    }
}

A criação dos middlewares Assert e Ensure, obriga que as rotas de api tenham o prefixo '/api/', para evitar redirecionamento

O terceiro parâmetro em JWT::assert e JWT::ensure, não deve ser informado se a autenticação for para API

Role Middleware

Será chamado em rotas onde o usuário precisa ter níveis de acesso específicos

Coloque após o 'Ensure'

$app->get('/dashboard','App\Callback\Page\Dashboard', ['App\Middleware\Token\Ensure', 'App\Middleware\Token\Role::admin']);
<?php

namespace App\Middleware;

use App\Service\Auth\Role as Service;
use Exception;

class Role
{
    public function handle($req, $res, $args)
    {
        try {
            $id = $req->query()->data->id;

            $role = new Service()->getRoleByUserId($id);

            if (!in_array($role, $args)) {
                $res->redirect('/me');
            }
        } catch (Exception $e) {
            throw new Exception($e->getMessage());
        }
    }
}

Query params

localhost:8080/books?filter=price

<?php

namespace App\Callback\Page;

use App\Service\Book\Read as Service;
use Exception;

class Book
{
    public function handle($req, $res)
    {
        try {
            $filter = $req->query()->filter ?? null;

            $data = new Service()->all($filter);

            $res->view('book', array('books' => $data));
        } catch (Exception $e) {
            throw new Exception($e->getMessage());
        }
    }
}

cURL

Verbos: GET, POST, PUT, PATCH e DELETE

<?php

namespace App\Callback\Payment;

use Nano\Core\Error;
use Exception;

class Create
{
    public function handle($req, $res)
    {
        try {
            $headers = ['Content-Type: application/json'];

            $body = json_encode([...]);

            $data = $req->http()->post('https://...', $headers, $body);

            if ($data) {
                $info = json_decode($data);

                $res->json($info->status, array('message' => $info->message));
            }

            throw new Exception('Erro ao realizar requisição');
        } catch (Exception $e) {
            Error::throwJsonException(500, $e->getMessage());
        }
    }
}

Validator

Regras: required, string, integer, float, bool, email, confirmed, min e max

Caso não houver erros na validação, um novo objeto será retornado em $req->data() com os dados 'sanitizados'

<?php

namespace App\Callback\Book;

use App\Service\Book\Create as Service;
use Nano\Core\Error;
use Exception;

class Create
{
    public function handle($req, $res)
    {
        try {
            $rules = [
                'title' => 'required|string',
                'description' => 'required|string|max:255',
                'authors' => [
                    'name' => 'required|string',
                    'website' => 'string'
                ]
            ];

            $req->validate($rules);

            $data = $req->data();

            $result = new Service()->register($data);

            $res->json(201, array('message' => $result));
        } catch (Exception $e) {
            Error::throwJsonException(500, $e->getMessage());
        }
    }
}
$rules = [
    'password' => 'required|string|min:8|confirmed'
];

O uso do confirmed exige um novo input, onde o 'name' precisa ter o sufixo '_confirmation'

<div class="field">
    <label for="password_confirmation">Confirmar senha</label>

    <input type="password" id="password_confirmation" name="password_confirmation" spellcheck="false" autocomplete="off">
</div>

Por padrão, as mensagens de erro estão em português. As opções aceitas são 'pt-BR' e 'en-US'

$req->validate($rules, 'en-US');

JSON Exception

Use throwJsonException para exibir erros no formato json

<?php

namespace App\Callback\Book;

use Nano\Core\Error;
use Exception;

class Read
{
    public function handle($req, $res)
    {
        try {
            $res->json(200, array());
        } catch (Exception $e) {
            Error::throwJsonException(500, $e->getMessage());
        }
    }
}

Template engine

O template utilizado foi desenvolvido por David Adams ( https://codeshack.io )

Foram feitas pequenas alterações no código original

base.html

<!DOCTYPE html>

<html lang="en-US">
    <head>
	<title>{% yield title %}</title>

        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
        <meta name="robots" content="noindex, nofollow">

        <link rel="stylesheet" href="/assets/style.css">
    </head>

    <body>
        {# comentário de teste #}
        <main>
            {% yield content %}
        </main>
    </body>
</html>

home.html

{% extends base %}

{% block title %}Nano Framework{% endblock %}

{% block content %}
<h1>{{ $welcome }}</h1>
{% endblock %}
<?php

namespace App\Callback\Page;

class Home
{
    public function handle($req, $res)
    {
        $res->view('home', array('welcome' => 'Welcome to Nano!'));
    }
}

Crie o diretório 'views'

Exibindo os erros de validação ( Validator )

{% foreach ($errors as $value): %}
    <div>{{ $value }}</div>
{% endforeach; %}

Para saber mais sobre este template engine, clique aqui

CORS

Coloque no index.php de sua API e modifique se necessário

header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE');
header('Access-Control-Allow-Headers: Origin, Accept, Content-Type, Authorization, X-Requested-With');

Login / Register

Exemplo de login e cadastro de usuário

GET /cadastro

<?php

namespace App\Callback\Page;

use Nano\Core\View\Form;

class Register
{
    public function handle($req, $res)
    {
        if ($req->hasCookie('token')) {
            $res->redirect('/me');
        }

        $form = Form::session($req);

        $res->view('register', [
            'csrf' => $form->csrf,
            'errors' => $form->errors
        ]);
    }
}

POST /cadastro

Ao usar Cookie para salvar o JWT, nomeie-o de 'token'. Isso é necessário pois há funções na classe JWT que busca, verifica e remove o cookie, pelo nome 'token'

<?php

namespace App\Callback\User;

use App\Service\User\Create as Service;
use Nano\Core\Error;
use Exception;

class Create
{
    public function handle($req, $res)
    {
        try {
            $rules = [
                'name' => 'required|string',
                'email' => 'required|email|confirmed',
                'password' => 'required|string|min:8|confirmed'
            ];

            $req->validate($rules);

            $data = $req->data();

            $token = new Service()->register($data);

            $req->setCookie('token', $token);

            $res->redirect('/me');
        } catch (Exception $e) {
            $req->setSession('errors', Error::parse($e->getMessage()));
            $res->redirect('/cadastro');
        }
    }
}

Service

<?php

namespace App\Service\User;

use Nano\Core\Database;
use Nano\Core\Security\JWT;
use Exception;
use PDO;

class Create
{
    private PDO $db;

    public function __construct()
    {
        $this->db = Database::instance();
    }

    public function register(object $data): string
    {
        $this->checkEmailExists($data->email);

        $hash = password_hash($data->password, PASSWORD_ARGON2ID);

        $query = "INSERT INTO users (name, email, password) VALUES (:name, :email, :password)";

        $stmt = $this->db->prepare($query);
        $stmt->bindValue(':name', $data->name, PDO::PARAM_STR);
        $stmt->bindValue(':email', $data->email, PDO::PARAM_STR);
        $stmt->bindValue(':password', $hash, PDO::PARAM_STR);

        if (!$stmt->execute()) {
            throw new Exception('Erro ao cadastrar, tente novamente');
        }

        $id = $this->db->lastInsertId();

        $data = array('id' => $id); // Payload

        $token = JWT::encode($data);

        return $token;
    }

    private function checkEmailExists(string $email): bool
    {
        $query = "SELECT id FROM users WHERE email = :email";

        $stmt = $this->db->prepare($query);
        $stmt->bindValue(':email', $email, PDO::PARAM_STR);
        $stmt->execute();

        $result = $stmt->fetch();

        if ($result > 0) {
            throw new Exception('O e-mail informado, já existe');
        }

        return true;
    }
}

GET /login

<?php

namespace App\Callback\Page;

use Nano\Core\View\Form;

class Login
{
    public function handle($req, $res)
    {
        if ($req->hasCookie('token')) {
            $res->redirect('/me');
        }

        $form = Form::session($req);

        $res->view('login', [
            'csrf' => $form->csrf,
            'errors' => $form->errors
        ]);
    }
}

POST /login

<?php

namespace App\Callback\Auth;

use App\Service\Auth\Login as Service;
use Nano\Core\Error;
use Exception;

class Login
{
    public function handle($req, $res)
    {
        try {
            $rules = [
                'email' => 'required|email',
                'password' => 'required|string'
            ];

            $req->validate($rules);

            $data = $req->data();

            $token = new Service()->authenticate($data);

            $req->setCookie('token', $token);

            $res->redirect('/me');
        } catch (Exception $e) {
            $req->setSession('errors', Error::parse($e->getMessage()));
            $res->redirect('/login');
        }
    }
}

Service

<?php

namespace App\Service\Auth;

use Nano\Core\Database;
use Nano\Core\Security\JWT;
use Exception;
use PDO;

class Login
{
    private PDO $db;

    public function __construct()
    {
        $this->db = Database::instance();
    }

    public function authenticate(object $data): string
    {
        $query = "SELECT id, password FROM users WHERE email = :email";

        $stmt = $this->db->prepare($query);
        $stmt->bindValue(':email', $data->email, PDO::PARAM_STR);
        $stmt->execute();

        $result = $stmt->fetchObject();

        if (!password_verify($data->password, $result->password)) {
            throw new Exception('E-mail ou senha inválido');
        }

        $data = array('id' => $result->id); // Payload

        $token = JWT::encode($data);

        return $token;
    }
}

GET /logout

<?php

namespace App\Callback\Auth;

class Logout
{
    public function handle($req, $res)
    {
        if ($req->hasCookie('token')) {
            $req->removeCookie('token');
            $res->redirect('/login');
        }

        $res->redirect('/');
    }
}

GET /me

<?php

namespace App\Callback\Page;

use App\Service\User\Read as Service;
use Exception;

class Me
{
    public function handle($req, $res)
    {
        try {
            $id = $req->query()->data->id;

            $data = new Service()->getUserInfoById($id);

            $res->view('me', $data);
        } catch (Exception $e) {
            throw new Exception($e->getMessage());
        }
    }
}