Luna é um framework desenvolvido em PHP com inspirações em outros frameworks como Laravel, CodeIgniter e o Express do JavaScript voltado para desenvolvimento Web com recursos como:
- Mapeamento de rotas;
- Banco de dados em ORM (Illuminate/Eloquent);
- Fila de middlewares;
- Praticidade, segurança e agilidade;
- Hospedagem simplificada;
- Armazenamento em cache;
- Componentização;
- Paginação;
- Search Engine Optimization (SEO).
- Aprendendo Luna (Documentação)
- Contribuindo com o projeto
- Licença
Antes de iniciar seu projeto Luna, é necessário realizar a instalação do PHP (versão 7.1 ou superior) e Composer.
Utilize o comando para iniciar um projeto com Luna:
composer create-project phpluna/luna {project-name}Renomeie o arquivo .env.example para .env e configure a URL conforme necessário:
mv .env.example .envCaso prefira, duplique o arquivo e mantenha o .env.example para que versione os exemplos de variáveis de ambiente.
As configurações gerais do projeto podem ser definidas no mesmo arquivo
.env, como por exemplo a chave de autenticação de alguma API de terceiros.
A criação de rota deve ser realizada em algum arquivo do diretório /routes (levando em consideração que você está seguindo o padrão apresentado aqui). É possível criar arquivos de separação, como pages.php para rotas de páginas ou api.php para rotas da API:
use App\Controllers\Pages;
use Luna\Http\Request;
use Luna\Http\Response;
$router->get('/', [
function(Request $request, Response $response) {
return Pages\HomeController::show($request, $response);
}
]);$router->get($uri, [$callback]);
$router->post($uri, [$callback]);
$router->put($uri, [$callback]);
$router->patch($uri, [$callback]);
$router->delete($uri, [$callback]);
$router->options($uri, [$callback]);Também é possível definir múltiplos métodos para um mesmo $uri e $callback:
$router->match(['get', 'post'], $uri, [$callback]);
$router->any($uri, [$callback]);As rotas podem receber parâmetros personalizados:
$router->get('/products/{id}', [
function(Request $request, Response $response) {
return Pages\ProductController::show($request, $response);
}
]);Os parâmetros podem ser obtidos na função executada:
class ProductController extends Page
{
public static function show(Request $request, Response $response) {
$id = $req->param('id');
}
}Caso prefira, também é possível obter os parâmetros da URL através de uma variável explicita:
$router->get('/products/{id}', [
function(int|string $id, Request $request, Response $response) {
return Pages\ProductController::show($id, $request, $response);
}
]);
class ProductController extends Page
{
public static function show(int|string $id, Request $request, Response $response) {
// ...
}
}Os parâmetros opcionais podem ser criados utilizando ?:
$router->get('/cart/{id?}', [
function(Request $request, Response $response) {
// ...
}
]);
$router->get('/product/{id}/{slug?}', [
function(Request $request, Response $response) {
// ...
}
]);Parâmetros opcionais não informados na requisição serão definidos como NULL.
Alguns erros comuns podem ser tratados diretamente na definição da rota para personalizar a página de retorno:
use App\Controllers\Pages\Errors;
use Luna\Http\Request;
use Luna\Http\Response;
$router->error(404, [
function(Request $request, Response $response) {
return Errors\NotFoundController::show($request, $response);
}
]);Também é possível definir uma rota padrão para qualquer erro:
$router->error('default', [
function(Request $request, Response $response) {
return Errors\General::show($request, $response);
}
]);Para realizar um redirecionamento em alguma rota, utilize a função redirect():
$router->get('/redirect', [
function(Rquest $request, Response $response) {
return $request->getRouter()->redirect('/destination');
}
]);Os middlewares fornecem um mecanismo conveniente para validar requisições em rotas específicas:
$router->get('/', [
'middlewares' => [ 'maintenance' ],
function(Request $request, Response $response) {
// ...
}
]);A classe do Middleware deve conter a função handle que será executada ao acessar a rota:
namespace App\Middlewares;
use Luna\Http\Request;
use Luna\Http\Response;
class Maintenance {
public function handle(Request $request, Response $response, $next) {
// ...
return $next($request, $response);
}
}A função handle deve receber os parâmetros $request, $response e $next e deve retornar $next($request, $response) para prosseguir com a fila.
Após criar a classe do Middleware, é necessário defini-lo com um apelido para que seja utilizado na definição da rota:
use Luna\Http\Middleware;
Middleware::setMap([
'maintenance' => \App\Middlewares\Maintenance::class
]);É possível definir middlewares padrões que serão executados em todas as rotas criadas:
Middleware::setDefault([
'maintenance'
]);O CORS pode ser configurado na inicialização da aplicação:
use Luna\Http\Cors;
Cors::setOrigins(["*"]); // Origens permitidas
Cors::setMethods(["*"]); // Métodos permitidos
Cors::setHeaders(["*"]); // Headers permitidos
Cors::setCredentials(true); // Aprovar credenciais
Cors::setMaxAge(0); // Tempo limite de cacheO Luna retornará automaticamente as requisições OPTIONS com os métodos disponíveis para uma rota.
O armazenamento do retorno de rotas em cache reduz o tempo de retorno para futuras requisições da mesma rota:
$router->get('/', [
'cache' => 10000,
function(Request $request, Response $response) {
// ...
}
]);O tempo de cache é definido em milisegundos
As configurações de cache podem ser definidas no arquivo .env:
| Configuração | Descrição |
|---|---|
| CACHE_TIME | Valor padrão de cache |
| CACHE_DIR | Diretório de armazenamento do cache |
| ALLOW_NO_CACHE_HEADER | Permitir o header Cache-Control: no-cache |
O valor de CACHE_TIME é definido como tempo de cache (também em milisegundos) quando o cache da rota for definido como true:
$router->get('/', [
'cache' => true,
function(Request $request, Response $response) {
// ...
}
]);As rotas executam (em sua maioria) Controllers:
namespace App\Controllers;
use Luna\Http\Request;
use Luna\Http\Response;
class ProductController
{
public static function show(Request $request, Response $response)
{
// ...
}
}Os dados da requisição como heaaders, parâmetros query, body e outros podem ser obtidos através da variável $request:
$request->header(); // Obter parâmetros do header
$request->query(); // Obter parâmetros da query
$request->body(); // Obter parâmetros do corpo
$request->param(); // Obter parâmetros da URL
$request->getUri(); // Obter URI
$request->getHttpMethod(); // Obter método HTTPÉ possível obter parâmetros específicos com as funções:
$request->query('id'). Não especificar um parâmetro fará com que todos sejam retornados em array.
Toda requisição deve ser respondida e sua resposta deve ser realizada no return da função do Controller:
class ProductController
{
public static function show(Request $request, Response $response)
{
// ...
return $response->send(200, "Sucesso");
}
}Recomenda-se seguir o padrão de Status HTTP (200 no exemplo) listados aqui.
As respostas da requisição podem retornar valores em text/html, application/json (mais comuns) ou outros (menos comuns):
public static function show(Request $request, Response $response)
{
// ...
return $response->send(200, [
"data" => "Sucesso"
], "application/json");
}É possível também utilizar alias para o retorno da requisição em HTML ou JSON:
$res->send(200, $content, 'json'); // Ao invés de 'application/json'
$res->send(200, $content, 'html'); // Ao invés de 'text/html'O tipo de resposta só deve ser informado na função caso o valor de DEFAULT_CONTENT_TYPE do arquivo .env seja diferente do desejado para o Controller.
Os Services auxiliam na obtenção e tratamento de dados entre o Banco de Dados e o Controller:
namespace App\Services;
class ProductService
{
public static function find(int|string $id)
{
// ...
}
}Uso do service:
use App\Services\ProductService;
use Luna\Http\Request;
use Luna\Http\Response;
class ProductController
{
public static function show(Request $request, Response $response)
{
$product = ProductService::find($request->param("id"));
return $response->send(200, $product);
}
}Um Helper agrupa pequenas funções úteis e que não são definidas como Services:
namespace App\Helpers;
class UuidHelper
{
public function generate()
{
// ...
}
}Uso do Helper:
use App\Helpers\UuidHelper;
class UserService
{
public function generateUuid()
{
return UuidHelper::generate();
}
}As views podem ser criadas em resources/views em .html e utilizadas na renderização:
namespace App\Controllers\Pages;
use Luna\Utils\View;
use Luna\Http\Request;
use Luna\Http\Response;
class ProductController
{
public static function show(Request $request, Response $response)
{
$content = View::render('pages/product', [
'name' => "Produto nome",
'description' => "Produto descrição"
]);
return $response->send(200, $content);
}
}Arquivo resources/view/page/product.html:
<h1>{{name}}</h1>
<p>{{description}}</p>
<!-- Resultado: -->
<!-- <h1>Produto nome</h1> -->
<!-- <p>Produto descrição</p> -->Para acessar diferentes níveis do array, utilize ->, por exemplo:
<p>{{name->first}}</p>
<p>{{phone->main->number}}</p>Obteria os valores, de:
$data = [
'phone' => [
'main' => [
'number' => '12345'
]
],
'name' => [
'full' => 'Fulano de Tal',
'first' => 'Fulano',
],
]Variáveis que não são enviadas podem receber um valor pré-definido com ??:
<p>{{name ?? Nome não definido}}</p>As variáveis da View seguem as mesmas regras dos Components, sendo assim, ambos conseguem utilizar todos os recursos.
A classe Page possui funções que permitem padronizar as páginas com header, footer e outros itens padrões, alterando o valor de content:
namespace App\Controllers\Pages;
use Luna\Utils\View;
use Luna\Http\Request;
use Luna\Http\Response;
class ProductController extends Page
{
public static function show(Request $request, Response $response)
{
$content = View::render('pages/product', [
'name' => "Produto nome",
'description' => "Produto descrição"
]);
$content = parent::getPage("Produto Título", $content);
return $response->send(200, $content);
}
}Com uso da classe Page a variável $content irá conter a junção dos arquivos page.html, header.html e footer.html (já existentes em /resources/view).
É possíve também adicionar novos arquivos padrões para cabeçalho e rodapé, podendo por exemplo criar diferentes cabeçalhos para a área pública e área administrativa:
$content = parent::getPage("Produto Título", $content, [
'header' => 'header-admin',
'footer' => 'footer-admin'
]);Para que os arquivos não sejam adicionadas, defina-o como false.
As variáveis mais comuns podem ser definidas no arquivo index.html em View::define() e podem ser utilizadas em qualquer View:
<img src="{{PUBLIC}}/assets/img/php-logo.png" />
<a href="{{URL}}"><button>Início</button></a>
<a href="{{URL}}/products"><button>Produtos</button></a>A Flash Message pode ser utilizada para retornar mensagens para a view de forma dinâmica:
namespace App\Controllers\Pages;
use Luna\Utils\Flash;
use Luna\Http\Request;
use Luna\Http\Response;
class ProductController
{
public static function show(Request $request, Response $response)
{
// ...
Flash::create("productNotFound", "Produto não encontrado", 'error');
}
}Após criar uma mensagem é possível renderiza-la para adicionar em uma view:
Flash::create("productNotFound", "Produto não encontrado", 'error');
$flash = Flash::render("productNotFound");É possível também renderizar uma mensagem que não tenha sido criada previamente:
$flash = Flash::render(false, "Produto não encontrado", 'error');O armazenamento das mensagens é realizado na variavel de sessão
$_SESSION, não cria-la previamente pode ser útil quando a mensagem não for utilizada em outros locais.
Caso deseje, renderize diversas mensagens de uma vez (apenas para mensagens criadas previamente):
$flashs = Flash::renderAll(["productNotFound", "productOutOfStock"]);Uma vez renderizada, adicione-a na view assim como outros parâmetros:
$content = View::render('pages/product', ['flash' => $flash]);Certifique-se de adicionar o parâmetro {{flash}} ou correspondente na view que será utilizada.
O componente das mensagens flashs pode ser alterado em /resources/components/flash/alert.html.
Se necessário, é possível criar um componente no mesmo diretório e seleciona-lo na renderização:
Flash::create("productNotFound", "Produto não encontrado", 'error', 'alert-new');
Flash::render("Produto não encontrado", 'error', 'alert-new');O valor de error presente nos exemplos é aplicado na variável {{type}} do componente e pode ser personalizado com qualquer valor para estilização.
Os tipos comuns são:
error,danger,warning,info,success.
Pequenas estruturas de uma view que sejam repetidas (ou não) podem ser utilizadas como um componente:
namespace App\Controllers\Pages;
use Luna\Utils\Component;
use Luna\Http\Request;
use Luna\Http\Response;
class ProductController
{
public static function show(Request $request, Response $response)
{
// ...
$productCard = Component::render('product-card', $product);
$content = View::render('pages/product', ['productCard' => $productCard]);
}
}O componente deve ser criado em .html assim como a view no diretório resources/components.
É possível também criar subpastas para organizar, por exemplo:
resources/components/product/carde renderizar comComponent::render('product/card', $product).
Em situações onde o mesmo componente deve ser renderizado diversas vezes a partir de um array:
$productCards = Component::multiRender('product-card', $products);
$content = View::render('pages/product', ['productCards' => $productCards]);A paginação de arrays para listagem pode ser realizada com uso da classe Pagination:
namespace App\Controllers\Pages;
use Luna\Utils\Pagination;
use Luna\Http\Request;
use Luna\Http\Response;
class ProductController
{
public static function show(Request $request, Response $response)
{
// ...
$pagination = new Pagination($products, $page, $limit);
$products = $pagination->get();
}
}A função get() retornará a lista já paginada e outros dados sobre a paginação.
É possível obter dados especificos da paginação:
$pagination->getCount(); // Obter quantidade de itens da página atual
$pagination->getList(); // Obter lista de itens da página atual
$pagination->getPages(); // Obter quantidade de páginas
$pagination->getPage(); // Obter página atual
$pagination->getLimit(); // Obter quantidade de itens por página
$pagination->getTotal(); // Obter quantidade total de itensO controle da paginação pode ser renderizado para ser exibido na View:
namespace App\Controllers\Pages;
use Luna\Utils\Pagination;
use Luna\Http\Request;
use Luna\Http\Response;
class ProductController
{
public static function show(Request $request, Response $response)
{
// ...
$pagination = new Pagination($products, $page, $limit);
$paginationRender = $pagination->render($req);
$content = View::render('pages/products', ['pagination' => $paginationRender]);
}
}Os componentes utilizados na criação da paginação podem ser modificados em resources/components/pagination e também podem ser alterados na renderização:
$paginationRender = $pagination->render($req, [
'last' => 'last.html'
// ...
]);O
hrefdos itens sempre utilizará o parâmetro {{page}} para definir a página destino.
Caso seja necessário remover algum item, defina o parâmetro como false.
Para limitar a quantidade de itens exibidos para cada lado do item atual, utilize:
$paginationRender = $pagination->render($req, [], 3);Exemplo de resultado da renderização com 3 itens para cada lado:
Estilização deve ser realizada separadamente
O acesso ao Banco de Dados de projetos Luna são realizados através do Object-Relational Mapping (ORM) utilizado no Laravel chamado Illuminate/Eloquent e o mesmo permite o uso de funções simples e rápidas para escrever querys SQLs complexas.
A configuração das credênciais de acesso ao banco de dados deve ser realizada no arquivo .env e a conexão é estabelecida com:
use Luna\Db\Database;
Database::boot();Ao utilizar o arquivo
bootstrap.phpa conexão é configurada por padrão. Ex:require __DIR__ . '/bootstrap.php';.
Acesse a documentação completo do Eloquent aqui.
A Migration pode ser utilizada para criar modificações no banco de dados de forma programática e versionada.
Para criar uma migration, utilize o Luna CLI:
php luna make:migration {{name}}O valor name deve conter o nome do arquivo de migração, por exemplo:
php luna make:migration create_users_tableÉ possível também criar a migration com uma tabela específica:
php luna make:migration --table=users add_role_id_column_to_users_tablePara realizar a migração do banco de dados utilize o comando:
php luna migrateIsto irá verificar quais migrações ainda não foram executadas e executa-las.
Para executar uma migração especifica, utilize o comando:
php luna migrate {{name}}Caso precise voltar atrás com a última migração realizada, utilize o comando:
php luna migrate:rollbackCada execução de
migrate:rollbackretornará um lote, ao executar o último lote será desfeito.
Para limpar o banco de dados e executar as migrações, utilize o comando:
php luna migrate:freshPor segurança, é necessário confirmar a execução do
migrate:freshcom--confirmou-c.
O Model segue o padrão do ORM Illuminate/Eloquent:
namespace App\Models;
use Luna\Db\Model;
class Product extends Model
{
// ...
}Uso do model:
namespace App\Services;
use App\Models\Product;
class ProductService
{
public function find(int|string $id) {
return Product::find($id);
}
public function list() {
return Product::all();
}
}O Search Engine Optimization (SEO) pode ser criado para exibição na View:
namespace App\Controllers;
use Luna\Utils\Seo;
use Luna\Http\Request;
use Luna\Http\Response;
class ProductController
{
public static function show(Request $request, Response $response)
{
// ...
$seo = new Seo();
$seo->setTitle("Produto nome");
$seo->setDescription("Produto descrição");
$seo->setKeywords(["produto-chave-1", "produto-chave-2"]);
$seo->setImage("produto.png");
$seo->setAuthor("Autor nome");
$seoRender = $seo->render();
$content = View::render('pages/product', ['seo' => $seoRender]);
}
}A função
$seo->setKeywords()pode receber as chaves em array ou em string, como por exemplo:$seo->setKeywords("chave-1, chave-2").
Caso não utilize um título definido separadamente na renderização da view em
parent::getPageutilize$seo->render(true)para que a tag<title>seja renderizada pelo SEO.
A configuração para Twitter e Meta OG podem ser realizadas separadamente:
$seo = new Seo();
$seo->setTitle("Produto nome");
$seo->twitter()->setTitle("Produto nome (Twitter)");
$seo->meta()->setTitle("Produto nome (Meta)");É possível configurar todas as tags separadamente para cada rede:
$seo->twitter()->setTitle($title);
$seo->twitter()->setDescription($description);
$seo->twitter()->setCard($card);
$seo->twitter()->setSite($site);
$seo->twitter()->setImage($image);
$seo->twitter()->setUrl($url);
$seo->meta()->setTitle($title);
$seo->meta()->setDescription($description);
$seo->meta()->setUrl($url);
$seo->meta()->setImage($image);
$seo->meta()->setType($type);Caso os dados para Twitter e Meta OG sejam iguais, basta informar de uma das seguintes formas:
$seo = new Seo();
$seo->twitter();
$seo->meta();
$seo->setTitle("Produto nome")
// ...$seo = new Seo(['twitter', 'meta']);
$seo->setTitle("Produto nome")
// ...Caso utilize
$seo->twitter()ou$seo->meta()após o uso de$seo->setTitle()e outros, as definições de título, descrição e imagem serão compartilhadas, para desativar essa função utilize $seo->twitter(false) ou $seo->meta(false) no primeiro uso de cada.
Caso a classe Seo seja inicializada sem definir o Twitter ou Meta OG o valor definido no arquivo .env em DEFAULT_SEO será utilizado.
A configuração de Robots podem ser adicionadas na renderização:
$seo = new Seo();
$seo->setRobots($index, $follow);As variáveis $index e $follow devem ser Boolean.
Exemplos de de definição do Robots:
// Página indexada e com links seguidos:
$seo->setRobots();
// Página não indexada:
$seo->setRobots(false);
// Página com links não seguidos:
$seo->setRobots(true, false);
// Página não indexada e links não seguidos:
$seo->setRobots(false, false);Por padrão a indexação utiliza links seguidos, então se for utilizar a função
$seo->setRobots()sem passar nenhum parâmetro, ela se torna dispensável.
O arquivo .env pode ser utilizado para definir valores de configurações do projeto e podem ser obtidas em arquivos:
use Luna\Utils\Environment;
Environment::get($key); // Obter item específico
Environment::get(); // Obter todos os itemsÉ possível também armazenar valores dinamicamente que não estejam presentes no arquivo .env:
Env::set($key, $value);Obrigado por considerar contribuir com o Luna! O guia de contribuição ainda encontra-se em desenvolvimento e em breve poderá entender como fazê-lo.
Enquanto isso, você pode realizar contribuições por conta própria no repositório.
O Luna é um software de código aberto sob a MIT License.
