palacios / framework
Framework web MVC moderno, tipado e modular para PHP 8.5
Requires
- php: ^8.5
- ext-json: *
- ext-mbstring: *
- ext-pdo: *
- ext-session: *
Requires (Dev)
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^12.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Framework web MVC moderno para PHP 8.5, com roteamento declarativo, injeção de dependência, model binding, validação, middleware, autenticação, autorização, proteção antiforgery, views, banco de dados, migrations e ferramentas de linha de comando.
O código da aplicação fica separado do framework: o Composer instala o núcleo em vendor/palacios/framework, enquanto seu projeto mantém apenas as regras de negócio, controllers, models, repositories, services e views.
Requisitos
Antes de começar, instale:
- PHP 8.5 ou superior;
- Composer 2;
- extensões PHP
json,mbstring,PDOesession; - um driver PDO para o banco escolhido, como
pdo_mysqloupdo_sqlite.
Confirme o ambiente:
php --version composer --version
Criando seu primeiro projeto
Instale a ferramenta globalmente:
composer global require palacios/framework:^0.1
Crie e execute uma aplicação:
framework new MinhaAplicacao
cd MinhaAplicacao
php -S localhost:8000 -t public
Acesse http://localhost:8000.
Se o comando framework não for encontrado, adicione o diretório global de binários do Composer ao PATH ou execute:
~/.config/composer/vendor/bin/framework new MinhaAplicacao
O comando new:
- copia a estrutura inicial da aplicação;
- cria o arquivo
.env; - gera automaticamente uma
APP_KEY; - configura a dependência da versão instalada;
- executa
composer install.
Use --no-install quando quiser instalar as dependências depois:
framework new MinhaAplicacao --no-install
Estrutura da aplicação
MinhaAplicacao/
├── app/
│ ├── Controllers/
│ ├── Middleware/
│ ├── Models/
│ ├── Repositories/
│ │ └── Interfaces/
│ ├── Services/
│ ├── Support/
│ └── Views/
│ └── layouts/
├── bootstrap/
│ └── app.php
├── database/
│ └── migrations/
├── public/
│ ├── assets/
│ └── index.php
├── resources/
├── tests/
├── .env
└── composer.json
Principais diretórios:
app/: código da sua aplicação;bootstrap/app.php: registro de serviços, middleware e rotas;database/migrations/: alterações versionadas do banco;public/: único diretório que deve ficar exposto pelo servidor web;resources/: arquivos de estilo e outros recursos;tests/: testes da aplicação;vendor/: dependências gerenciadas pelo Composer; não edite esse diretório.
Como uma requisição é processada
O arquivo public/index.php inicia a aplicação definida em bootstrap/app.php. O pipeline executa os middleware habilitados, encontra a rota correspondente, resolve as dependências do controller, converte os dados da requisição em parâmetros tipados e produz a resposta HTTP.
A ordem das chamadas use*() em bootstrap/app.php também é a ordem do pipeline.
Configuração inicial
O projeto gerado carrega as variáveis do arquivo .env e cria a aplicação:
<?php use Palacios\Framework\App; use Palacios\Framework\Hosting\ApplicationEnvironment; $builder = App::createBuilder( dirname(__DIR__), $_ENV['APP_ENV'] ?? ApplicationEnvironment::PRODUCTION, ); $builder->services() ->addControllers() ->addViews() ->addAuthentication() ->addAuthorization(); $app = $builder->build(); if ($builder->environment->isDevelopment()) { $app->useDevelopmentExceptionPage(); } else { $app->useExceptionHandler(); } $app->mapControllers(); return $app;
O arquivo criado pelo comando new já contém uma configuração funcional. Habilite somente os middleware necessários ao projeto.
Criando um controller
Gere o arquivo:
framework make:controller Users
Defina o prefixo e as ações com atributos:
<?php namespace App\Controllers; use Palacios\Framework\Results\IActionResult; use Palacios\Framework\Results\Results; use Palacios\Framework\Routing\Attributes\HttpGet; use Palacios\Framework\Routing\Attributes\Route; #[Route('/users')] final class UsersController { #[HttpGet(name: 'users.index')] public function index(): IActionResult { return Results::json([ ['id' => 1, 'name' => 'Ana'], ['id' => 2, 'name' => 'Carlos'], ]); } #[HttpGet('/{id:int}', name: 'users.show')] public function show(int $id): IActionResult { return Results::json(['id' => $id]); } }
addControllers() descobre as classes em app/Controllers e mapControllers() registra suas rotas.
Rotas diretas e grupos
Rotas também podem ser declaradas no bootstrap:
$app->mapGet('/status', static fn () => Results::ok([ 'status' => 'online', ])); $app->mapPost('/users', [UsersController::class, 'store']);
Agrupe endpoints que compartilham prefixo ou regras:
$api = $app->mapGroup('/api') ->withTags('API'); $v1 = $api->mapGroup('/v1'); $v1->mapGet('/users', [UsersController::class, 'index']); $v1->mapGet('/users/{id:int}', [UsersController::class, 'show']);
Rotas nomeadas permitem gerar URLs sem repetir caminhos:
$url = $app->urlFor('users.show', ['id' => 42]);
Injeção de dependência
Registre contratos e implementações em bootstrap/app.php:
$builder->services() ->addScoped(IUserRepository::class, UserRepository::class) ->addTransient(Mailer::class, Mailer::class) ->addSingleton(Cache::class, Cache::class);
Tempos de vida disponíveis:
addScoped: uma instância por requisição;addTransient: uma nova instância a cada resolução;addSingleton: uma única instância durante a aplicação.
Depois, solicite a dependência pelo construtor:
final readonly class UsersController { public function __construct( private IUserRepository $users, ) {} }
Interfaces, repositories e services podem ser usados sem DTOs. DTOs são opcionais e úteis quando você quer representar e validar dados de entrada de forma explícita.
Recebendo dados da requisição
Parâmetros tipados são preenchidos automaticamente por convenção a partir da rota, query string, formulário, JSON ou container:
#[HttpGet('/{id:int}')] public function show(int $id): IActionResult { return Results::json(['id' => $id]); }
Quando houver ambiguidade, indique a origem:
#[FromRoute]: parâmetro da rota;#[FromQuery]: query string;#[FromForm]: formulário;#[FromBody]: corpo JSON;#[FromHeader]: cabeçalho HTTP;#[FromServices]: container de serviços.
Em código bem tipado e sem ambiguidade, esses atributos não são obrigatórios.
DTOs e validação
Um DTO pode centralizar os dados e suas regras:
use Palacios\Framework\Validation\Attributes\EmailAddress; use Palacios\Framework\Validation\Attributes\Required; use Palacios\Framework\Validation\Attributes\StringLength; final class CreateUserInput { public function __construct( #[Required] #[StringLength(maximumLength: 120, minimumLength: 2)] public string $name, #[Required] #[EmailAddress] public string $email, ) {} }
Use DTOs quando eles deixarem a entrada mais clara. Para ações simples, parâmetros escalares tipados ou services injetados continuam válidos.
Views
Retorne uma view passando seus dados:
return Results::view('users/index', [ 'users' => $this->users->all(), ]);
No arquivo app/Views/users/index.php:
<h1>Usuários</h1> <?php foreach ($users as $user): ?> <p><?= e($user->name) ?></p> <?php endforeach; ?>
O helper e() escapa conteúdo para HTML. Os layouts ficam, por padrão, em app/Views/layouts.
Se a view ou o layout lançar uma exceção, o framework descarta o HTML parcial e encaminha a exceção ao tratamento global. Os buffers existentes antes da renderização são preservados. Templates que criam buffers adicionais devem mantê-los removíveis (o padrão de ob_start()) e não fechar os buffers do renderizador.
Formulários e proteção antiforgery
Ative o middleware antes de mapear os controllers:
$app->useAntiforgery(); $app->mapControllers();
Inclua o campo oculto em formulários que alteram dados:
<form method="post" action="/users">
<?= csrf_input() ?>
<input type="text" name="name">
<button type="submit">Salvar</button>
</form>
O framework valida automaticamente o token nas requisições protegidas. O campo interno _csrf não é incluído no model binding, portanto o controller recebe apenas os dados do formulário e não precisa remover o token manualmente.
O próprio helper obtém e reutiliza o token guardado na sessão. Não gere o token no controller nem o envie como dado da view. Se a geração criptograficamente segura falhar, o framework interrompe a operação e o tratamento global devolve 500; ele nunca renderiza um token vazio nem usa uma alternativa insegura.
Autenticação e autorização
Registre os serviços:
$builder->services() ->addAuthentication( static fn (AuthenticationOptions $options) => $options ->loginPath('/login') ->accessDeniedPath('/acesso-negado'), ) ->addAuthorization();
Adicione os middleware nesta ordem:
$app->useAuthentication(); $app->useAuthorization();
Proteja endpoints:
$app->mapGet('/conta', $handler) ->requireAuthorization(); $app->mapGet('/admin', $handler) ->requireAuthorization('admin'); $app->mapGet('/entrar', $loginHandler) ->allowAnonymous();
Controllers também podem usar #[Authorize]. Sem páginas de interface configuradas, acessos negados retornam 401 ou 403; com loginPath() e accessDeniedPath(), aplicações web recebem redirecionamentos.
Middleware
Crie um middleware:
framework make:middleware RequestTrace
Registre middleware globais no pipeline ou associe-os a grupos de rotas:
$api = $app->mapGroup('/api') ->addMiddleware(RequestTraceMiddleware::class);
Use middleware para responsabilidades transversais, como logs, autenticação, autorização, antiforgery, cabeçalhos e tratamento de erros.
Logging de requisições
Ative o registro de requisições no bootstrap da aplicação:
$app->useRequestLogging(); $app->mapControllers();
Sem configuração adicional, o framework registra uma linha JSON no log de erros do PHP usando a categoria HTTP. Para usar outra implementação, registre-a antes de construir a aplicação:
use Palacios\Framework\Logging\Logger; $builder->services()->addSingleton(Logger::class, MyApplicationLogger::class); $app = $builder->build(); $app->useRequestLogging();
A implementação personalizada deve implementar a interface Logger. O registro da aplicação sempre substitui o logger padrão do middleware.
O logging envolve todo o processamento HTTP, incluindo respostas 404 e 405. Cada requisição gera um único registro com método, caminho, status final, duração e traceId; chamadas repetidas a useRequestLogging() não duplicam o registro.
Banco de dados
Configure o .env:
DB_DRIVER=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=minha_aplicacao DB_USERNAME=root DB_PASSWORD=secret
Também é possível informar uma DSN completa:
DB_DSN=sqlite:database/database.sqlite
Registre a conexão:
$builder->services()->addDatabase( static fn (DatabaseOptions $options) => $options ->dsn($_ENV['DB_DSN']) ->username($_ENV['DB_USERNAME'] ?? null) ->password($_ENV['DB_PASSWORD'] ?? null), );
Repositories podem receber PDO diretamente pelo construtor.
Migrations
Gere uma migration:
framework make:migration CreateUsers
Implemente as alterações:
<?php use Palacios\Framework\Database\Migration; return new class implements Migration { public function up(PDO $connection): void { $connection->exec( 'CREATE TABLE users (id INTEGER PRIMARY KEY, name VARCHAR(120) NOT NULL)', ); } public function down(PDO $connection): void { $connection->exec('DROP TABLE users'); } };
Aplique as migrations pendentes:
framework database:update
O histórico das migrations executadas fica na tabela framework_migrations.
Geradores disponíveis
framework make:controller Admin/Users framework make:model User framework make:service User framework make:repository User framework make:middleware RequestTrace framework make:migration CreateUsers
Os geradores usam namespaces PSR-4 e não sobrescrevem arquivos existentes. make:repository cria a interface e a implementação.
Resultados HTTP
Handlers e controllers podem retornar implementações de IActionResult:
return Results::ok($value); return Results::json($value); return Results::created('/users/1', $value); return Results::noContent(); return Results::badRequest($errors); return Results::unauthorized(); return Results::forbid(); return Results::notFound(); return Results::redirect('/login'); return Results::view('users/index', ['users' => $users]);
Exceções HTTP são convertidas em respostas application/problem+json. Detalhes internos ficam visíveis somente no ambiente de desenvolvimento.
Executando com Docker
O projeto inicial inclui Dockerfile e docker-compose.yml:
docker compose up --build
Ajuste as variáveis do banco no .env conforme o ambiente utilizado.
Testes
Execute os testes da aplicação:
composer test:smoke
Para testes de integração, use WebApplicationFactory:
$factory = new WebApplicationFactory( static function (WebApplication $app): void { $app->mapGet('/ping', static fn () => Results::json(['status' => 'ok']) ); }, ); $response = $factory->createClient()->get('/ping');
No repositório do framework, o teste de emissão HTTP com Apache pode ser executado no Linux ou Ubuntu WSL com PHP CLI e Docker:
composer test:apache
Ele inicia um container descartável com php:8.5.12-apache, verifica a resposta recebida pela rede (inclusive o status 419 de formulário expirado) e remove o container ao terminar. Para testar outra imagem compatível com PHP/Apache, defina FRAMEWORK_APACHE_IMAGE. Esse teste também é executado na CI; testes em memória ou no servidor embutido não substituem essa validação do emissor.
Ambientes
Defina o ambiente no .env:
APP_ENV=Development
Valores comuns:
Development: página detalhada de exceções;Production: respostas seguras sem detalhes internos.
Nunca habilite a página detalhada de exceções em produção.
Servidor web em produção
Configure Apache, Nginx ou outro servidor para usar public/ como document root. Não exponha a raiz do projeto, o arquivo .env, vendor/ ou app/ diretamente pela web.
O servidor embutido do PHP é indicado apenas para desenvolvimento local:
php -S localhost:8000 -t public
Atualizando o framework
Verifique versões disponíveis:
composer outdated palacios/framework
Atualize dentro da restrição permitida pelo projeto:
composer update palacios/framework
Leia o changelog antes de atualizar entre versões menores enquanto o projeto estiver na série 0.x.
Ajuda e segurança
- Consulte o CHANGELOG para alterações por versão.
- Consulte a política de segurança para reportar vulnerabilidades.
- O projeto é distribuído sob a licença MIT.
A série 0.x representa a fase inicial da API. Mudanças incompatíveis podem ocorrer em versões menores até a estabilização da versão 1.0.0.