An open API service indexing awesome lists of open source software.

https://github.com/jonathansilva/nano

Nano framework PHP para desenvolvimento de API's e aplicações Web
https://github.com/jonathansilva/nano

framework framework-php rest-api vanilla-php web-development

Last synced: 6 months ago
JSON representation

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

Awesome Lists containing this project

README

          

# Nano

*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*

```apache
RewriteEngine On
Options All -Indexes

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

*Nginx*

```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
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' => 'Hello World!'));
});

$app->start();
```

# Routes

Verbos: GET, POST, PUT, PATCH e DELETE

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

O Callback/Controller não permite chamada de método

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

✔️

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

Crie o método 'handle'

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

## Routes file

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

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

routes.xml

```xml


/
GET
App\Callback\Page\Home


/me
GET
App\Callback\Page\Me

App\Middleware\Token\Ensure


/dashboard
GET
App\Callback\Page\Dashboard

App\Middleware\Token\Ensure
App\Middleware\Role::admin

```

# Middleware

Middlewares devem ser informados no terceiro parâmetro da rota ( [Routes](#routes) )

Para configurar um middleware **global**, utilize o método 'use'

```php
$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'

```html

```

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

*JWT*

1 ) 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](#role-middleware) )

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

```php
O terceiro parâmetro em `JWT::assert` é ignorado em rotas de api

## 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
O terceiro parâmetro em `JWT::ensure` é ignorado em rotas de api

## Role Middleware

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

> Coloque após o 'Ensure'

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

```php
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
query()->filter ?? null;

$res->view('books', array('books' => new Service()->all($filter)));
} catch (Exception $e) {
throw new Exception($e->getMessage());
}
}
}
```

# cURL

Verbos: GET, POST, PUT, PATCH e DELETE

```php
http()->post($url, $headers, $body);

if (!$data) {
throw new Exception('Erro ao realizar requisição');
}

$info = json_decode($data);

$res->json($info->status, array('message' => $info->message));
} 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](https://github.com/jonathansilva/nano/blob/master/src/Core/Security/Sanitize.php)

```php
'required|string',
'description' => 'required|string|max:255',
'authors' => [
'name' => 'required|string',
'website' => 'string'
]
];

$req->validate($rules);

$data = $req->data();

$res->json(201, array('message' => new Service()->register($data)));
} catch (Exception $e) {
Error::throwJsonException(500, $e->getMessage());
}
}
}
```

```php
$rules = [
'password' => 'required|string|min:8|confirmed'
];
```

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

```html


Confirmar senha



```

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

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

# JSON Exception

Use `throwJsonException` para exibir erros no formato json

```php
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

```html


{% yield title %}





{# comentário de teste #}

{% yield content %}

```

home.html

```html
{% extends base %}

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

{% block content %}

{{ $welcome }}


{% endblock %}
```

```php
view('home', array('welcome' => 'Welcome to Nano!'));
}
}
```

> Crie o diretório 'views'

Exibindo os erros de validação ( [Validator](#validator) )

```html
{% foreach ($errors as $value): %}

{{ $value }}

{% endforeach; %}
```

Para saber mais sobre este template engine, [clique aqui](https://codeshack.io/lightweight-template-engine-php)

# CORS

Coloque no index.php de sua API e faça as modificações necessárias

```php
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
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
'required|string',
'email' => 'required|email|confirmed',
'password' => 'required|string|min:8|confirmed'
];

$req->validate($rules);

$data = $req->data();

$req->setCookie('token', new Service()->register($data));
$res->redirect('/me');
} catch (Exception $e) {
$req->setSession('errors', Error::parse($e->getMessage()));
$res->redirect('/cadastro');
}
}
}
```

Service

```php
db = Database::instance();
}

public function register(object $data): string
{
if ($this->emailExists($data->email)) {
throw new Exception('O e-mail informado já existe');
}

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

try {
$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);
$stmt->execute();

return JWT::encode(array('id' => $this->db->lastInsertId()));
} catch (PDOException $e) {
throw new Exception('Erro ao cadastrar, tente novamente');
}
}

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

$stmt = $this->db->prepare($query);
$stmt->execute(array(':email' => $email));

return (bool) $stmt->fetchColumn();
}
}
```

GET /login

```php
hasCookie('token')) {
$res->redirect('/me');
}

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

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

POST /login

```php
'required|email',
'password' => 'required|string'
];

$req->validate($rules);

$data = $req->data();

$req->setCookie('token', new Service()->authenticate($data));
$res->redirect('/me');
} catch (Exception $e) {
$req->setSession('errors', Error::parse($e->getMessage()));
$res->redirect('/login');
}
}
}
```

Service

```php
db = Database::instance();
}

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

$stmt = $this->db->prepare($query);
$stmt->execute(array(':email' => $data->email));

$result = $stmt->fetchObject();

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

return JWT::encode(array('id' => $result->id));
}
}
```

GET /logout

```php
hasCookie('token')) {
$req->removeCookie('token');
$res->redirect('/login');
}

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

GET /me

```php
query()->data->id;

$res->view('me', array('data' => new Service()->getUserInfoById($id)));
} catch (Exception $e) {
throw new Exception($e->getMessage());
}
}
}
```