PHP Quick Development

Maintainers

Package info

github.com/willkerms/PQD

pkg:composer/willkerms/pqd

Statistics

Installs: 464

Dependents: 2

Suggesters: 0

Stars: 3

Open Issues: 0

1.4 2023-06-14 14:05 UTC

This package is auto-updated.

Last update: 2026-06-01 20:40:42 UTC


README

Framework PHP minimalista para desenvolvimento rápido de aplicações web, focado em:

  • Roteamento por convenção (URL → Controller, sem tabela de rotas).
  • ORM próprio sobre PDO, com builder de SQL legível.
  • Multi-ambiente / multi-host nativo: a mesma instalação atende várias aplicações.
  • MVC enxuto: Entity, BO, DAO, Controller, View — cada um com uma única responsabilidade.

Repositório: https://github.com/willkerms/pqd Licença: GPL-2.0-or-later Autor: Willker Moraes Silva

Instalação

composer require willkerms/pqd:dev-master

Requer PHP 8.0+.

Bootstrap

PQD não tem bin/console ou esqueleto: você cria um único arquivo de bootstrap (ex.: req_default.php) e o public/index.php apenas faz require_once.

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

use PQD\PQDApp;

// Cria o app. O segundo argumento lista os ambientes válidos.
$oApp = PQDApp::run($_SERVER['REQUEST_URI'], ['admin', '']);

// Mapeia os ambientes para pastas dentro de modulos/
// '' = modulos/ raiz, 'admin' = modulos/admin/, etc.
$oApp->setHostsEnv([
    'admin.exemplo.com' => 'admin',
    'app.exemplo.com'   => '',
]);

// Conexão de banco (suporta mssql, mysql, pgsql via PDO)
$oApp->setDbConnection('mysql', 'localhost', 'meu_banco', 'usuario', 'senha');

// Templates de header/footer aplicados em todas as views
$oApp->setTemplate('templates/tpl.head.php', 'templates/tpl.footer.php');

// Caminhos que não exigem sessão autenticada
$oApp->addFreePaths('/login', '/api/public');

$oApp->exec();

PQDApp::run() é estático e retorna a instância única do aplicativo — não pode ser instanciada manualmente.

Roteamento por convenção

Não há tabela de rotas. A URL é mapeada diretamente para a estrutura de pastas:

URL Mapeamento
/catalog/products modulos/catalog/products/ProductsCtrl::view()
/catalog/products?act=form&id=5 modulos/catalog/products/ProductsCtrl::form()
/catalog/products?act=salvar modulos/catalog/products/ProductsCtrl::salvar()

Regras:

  • Cada segmento de URL após o ambiente vira uma pasta (kebab-case → camelCase).
  • O último segmento é o nome do controller, em singular para a pasta e plural para o arquivo <Entidades>Ctrl.php.
  • O parâmetro ?act= define o método chamado. Sem act, o método padrão é view().

Camadas

PQD impõe uma separação rígida entre cinco arquivos por caso de uso:

modulos/<grupo>/<caso>/
├── <Entity>.php          # Entity   (PQDEntity)        — mapeia tabela
├── <Entity>DTO.php       # DTO       (extends Entity)  — adiciona campos de JOIN
├── <Entities>BO.php      # Business  (PQDBo)           — regras de negócio
├── <Entities>DAO.php     # Data      (PQDDAO)          — SQL / persistência
├── <Entities>Ctrl.php    # Controller (PQDController) — HTTP
└── view/
    ├── <Entities>Grid.php
    ├── <Entity>View.php
    └── <Entities>PDF.php

Entity (PQDEntity)

Representa uma tabela. Atributos camelCase mapeiam colunas. Anotações no phpDOC alimentam o ORM e as views:

/**
 * @entity(isEscaped=true)
 * @table(name=products, clsDTO=modulos\catalog\products\ProductDTO)
 */
class Product extends PQDEntity {

    /**
     * @field(name=idProduct, description=ID, type=int, isNotNull=true)
     */
    protected $idProduct;

    /**
     * @field(name=price, description=Price, type=money, precision=2, required=true, isFilter=true)
     */
    protected $price;

    /**
     * @field(name=name, description=Name, type=string, required=true)
     */
    protected $name;

    // getters/setters camelCase; setters retornam $this para encadeamento
}

Métodos úteis herdados:

  • toArray() — serializa em array.
  • toJSON() — serializa em JSON.
  • escapeHTML() — HTML-escape em todos os atributos string (in-place).

Propriedades suportadas dentro de @field(...) (lista resumida):

Propriedade Função
name Nome real da coluna. Em DTOs pode incluir alias de tabela: c.name
alias Alias de retorno em DTOs: name=c.name, alias=categoryc.name AS category
description Label do campo (use entidades HTML para acentos)
type int, string, money, float, date, datetime, boolean
precision Casas decimais para money / float
isNotNull Coluna NOT NULL no banco
required Obrigatório na view, adiciona * no label
requiredCaption Caption da mensagem de validação quando o campo é virtual (ex.: FK)
virtual Campo não persistido (só leitura ou calculado) — não vai ao banco
isFilter Aparece no modal de filtros avançados
filterOrder Ordem do campo na lista de filtros

Anotações complementares: @help e @list

@help e @list são anotações independentes — cada uma em sua própria linha do phpDOC, logo abaixo do @field. Não são propriedades de @field(...).

/**
 * @field(name=status, description=Status, type=int, isNotNull=true, isFilter=true)
 * @list(["Inactive", "Active", "Archived"])
 * @help(Current status of the record)
 * @var int
 */
protected $status;

@help(texto) — texto livre (uma linha) exibido como tooltip ao lado do label. Caracteres acentuados podem ser escritos como entidades HTML. Apenas uma anotação @help por campo é lida pelo parser.

@list(...) — declara a lista de valores de um campo enum/select. Duas formas suportadas pelo PQD:

Forma Comportamento
JSON inline O conteúdo entre parênteses é decodificado diretamente como JSON. Pode ser um array indexado (@list(["Inactive", "Active", "Archived"])), um objeto chave→valor, ou qualquer estrutura aceita por json_decode.
Arquivo .json Se o conteúdo for uma string terminando em .json (ex.: @list("status.json")), PQD carrega o arquivo a partir do diretório da classe da entidade e usa seu conteúdo como lista. Lança exceção se o arquivo não existir.

Aplicações podem estender o parser (subclasse de PQDAnnotation) para resolver @list a partir de outras fontes — por exemplo, sobrescrever getFields() e, quando o valor decodificado de list ainda for uma string (não array), buscá-la em banco de dados, API ou cache.

DTO

Estende a Entity e adiciona campos vindos de JOIN. As anotações seguem o mesmo formato, mas com aliases:

/**
 * @field(name=c.name, alias=category, description=Category, type=string)
 */
protected $category;

Use virtual=true para que a validação client-side ignore o campo e valide pela FK correspondente.

DAO (PQDDAOPQD\SQL\SQLSelect)

Responsável por SQL. Métodos principais herdados:

  • save(PQDEntity &$entity) — INSERT ou UPDATE, retorna bool.
  • delete(PQDEntity $entity) — DELETE, retorna bool.
  • genericSearch(SQLWhere $where, ?array $fields=null, $fetchClass=true, ?SQLOrderBy $orderBy=null, $limit=null, $page=0, ?SQLGroupBy $groupBy=null) — SELECT genérico.
  • retEntity($id, $fetchClass=true, ?array $fields=null) — busca um único registro pela PK.
  • retNumReg(SQLWhere $where)COUNT(*) com os mesmos filtros.

Joins ficam declarados no construtor do DAO via PQD\SQL\SQLJoin:

public function __construct(...) {
    parent::__construct(...);

    $oJoin = new SQLJoin('p'); // alias da tabela principal
    $oJoin->setLeftJoin('categories c', 'c.idCategory = p.idCategory');
    $this->setDefaultWhereOnSelect($oJoin);
}

BO (PQDBo implementa IPQDBo)

Camada de regras de negócio. Recebe \stdClass $oSession (para validação de permissões) e PQDExceptions $oExceptions no construtor. Convenção de métodos por caso de uso:

Método Função
new<Entity>() Instancia entidade com defaults
formatView($entity) Converte campos do formato banco para o formato de exibição
formatDB($entity) Converte campos do formato view para o formato banco
isValid($entity) Validações de regra de negócio antes de persistir
save(&$entity) Chama formatDB, isValid e getDAO()->save()
delete(&$entity) Soft ou hard delete via DAO
search($q, $type, ...) Lista paginada padrão usada pelo controller

PQDBo declara getDAO() como abstrato — cada BO concreto retorna sua instância de DAO.

Controller (PQDController implementa IPQDController)

Recebe os superglobais já encapsulados:

  • $this->get — clone de $_GET.
  • $this->post — clone de $_POST.
  • $this->session\stdClass da sessão.
  • $this->exceptions — instância de PQDExceptions.

Métodos comuns:

public function view()    { /* lista (Grid)         */ }
public function novo()    { /* form vazio           */ }
public function form()    { /* form preenchido      */ }
public function salvar()  { /* persiste o $_POST    */ }
public function excluir() { /* deleta               */ }
public function search()  { /* JSON da busca        */ }
public function xls()     { /* exportação Excel     */ }

setView($path, $autoRender=true, $requireHeaderAndFooter=true) instancia a view; getView() devolve a instância para atribuir variáveis ($this->getView()->oProduct = ...).

View (PQDView)

A view é apenas um arquivo PHP. Variáveis atribuídas via $controller->getView()->foo = $bar viram $this->foo dentro do arquivo. render() injeta os templates de header/footer registrados em PQDApp::setTemplate().

Helpers úteis:

  • $this->e($field, $escape=null, $charset='UTF-8') — devolve a description do campo a partir dos metadados ($this->fields).
  • Acesso dinâmico via __get / __set permite injetar qualquer propriedade.

Convenções de nomenclatura de variáveis

PQD adota um esquema enxuto de prefixos por tipo nas variáveis locais, inspirado em uma versão leve de Hungarian notation. O objetivo é deixar o tipo da variável visível no nome, sem depender da declaração ou do retorno de uma função para inferi-lo.

Tipo Prefixo Exemplos
Objeto / instância $o<Nome> $oCliente, $oWhere, $oOrderBy
Array $a<Nome> $aFields, $aDados, $aClientes
Boolean $is<X> ou $has<X> (inglês preferido) $isCodigo, $hasNome, $isAtivo
Data (date) $dta<Nome> $dtaInicio, $dtaFim, $dtaCadastro
Timestamp / datetime $time<Nome> $timeInicio, $timeUltimaAlt
String SQL $sql (com sufixo opcional) $sql, $sqlInsert, $sqlContagem
Primitivos (int, string, float) camelCase plain $idCliente, $titulo, $status, $valor

Boolean é predicado. Sempre começa com is ou has em inglês — o padrão recomendado é o inglês porque a expressão lê naturalmente como pergunta (if ($isCodigo) → "se é código"). Versões em português ($ehCodigo, $temNome) são aceitas, mas não recomendadas.

SQL pode ter sufixo quando há mais de uma query no mesmo escopo ($sqlInsert, $sqlUpdate, $sqlContagem), mas o nome sempre começa com $sql — esse é o sinal de que a variável carrega SQL cru.

Primitivos não levam prefixo de tipo. O nome do campo já carrega a semântica ($idCliente é claramente um ID inteiro; $titulo é claramente uma string). Prefixar $i ou $s polui o nome sem ganho.

Atributos de classe seguem a mesma regra. Ex.: $this->oWhere, $this->aFields, $this->isCodigo.

ORM — Builders de SQL

SQLWhere

$oWhere = new SQLWhere();
$oWhere->setEqual('idCategory', 5, SQLWhere::NUMBER)
       ->setAnd()
       ->setBetween('createdAt', ['2024-01-01', '2024-12-31'], SQLWhere::BETWEEN)
       ->setAnd()
       ->setLike('name', 'Pro%', SQLWhere::LIKE)
       ->setAnd()
       ->setParentheses()
           ->setIn('status', [1, 2, 3], SQLWhere::IN)
           ->setOr()
           ->setIsNull('archivedAt')
       ->setParentheses(true);

$oDAO->genericSearch($oWhere);

Operadores: setEqual, setDiff (<>), setMore (>), setMoreEqual (>=), setLess (<), setLessEqual (<=), setLike, setBetween, setIn, setIsNull, setSQL (raw fragment). Conectores: setAnd(), setOr(), setParentheses($close=false).

Constantes de tipo: SQLWhere::STRING (envolve com aspas e escapa), ::NUMBER, ::DATE, ::DATETIME, ::IN, ::BETWEEN, ::IS.

SQLOrderBy

$oOrderBy = new SQLOrderBy(['price', 'createdAt'], false); // false = DESC
$oOrderBy->addField('name');
$oDAO->genericSearch($oWhere, null, true, $oOrderBy);

SQLJoin extends SQLWhere

$oJoin = new SQLJoin('p');
$oJoin->setLeftJoin('categories c', 'c.idCategory = p.idCategory');
$oJoin->setInnerJoin('brands b', 'b.idBrand = p.idBrand');

O construtor recebe o alias da tabela principal — não passe o nome da tabela.

SQLGroupBy

Análogo a SQLOrderBy; passado como sétimo argumento de genericSearch.

Conexões e PDO

PQDApp::setDbConnection($driver, $host, $database, $user, $password) retorna o índice da conexão. Aplicações que falam com múltiplos bancos devem guardar esses índices em constantes:

define('DB_DEFAULT', $oApp->setDbConnection('mysql', 'host1', 'db1', 'u', 'p'));
define('DB_LOGS',    $oApp->setDbConnection('mysql', 'host2', 'db2', 'u', 'p'));

PQDDb::getConnection($index = 0) devolve um PQDPDO (extensão de \PDO). PQDPDO reescreve query() e exec() para registrar erros automaticamente no objeto de exceções da aplicação.

Exceções

PQDApp cria uma instância única de PQDExceptions e a injeta em todos os Controllers, BOs e DAOs. Há três classes hierárquicas:

Classe Quando usar
PQDExceptions Exceção de usuário final (mensagem segura para exibir)
PQDExceptionsDev Exceção interna que sinaliza erro de programação
PQDExceptionsDB Erros oriundos do banco; já capturados por PQDPDO::query() e exec()

Padrão no controller:

try {
    if ($this->getBO()->save($oEntity)) {
        $this->getView()->setMessage('Salvo!', 'success');
    }
}
catch (PQDExceptionsDev $e) {
    $this->exceptions->setException($e);
}

Multi-ambiente e free paths

O método PQDApp::setHostsEnv([$host => $envFolder, ...]) permite que a mesma instalação atenda múltiplos hostnames, cada um direcionado a uma pasta diferente dentro de modulos/. Ambientes diferentes podem ter:

  • Assets públicos próprios (ex.: public/<env>/).
  • Templates, views e controllers próprios.
  • Conjunto de telas e permissões próprios.

PQDApp::addFreePaths('/login', '/recuperar-senha', ...) lista URLs que não exigem sessão autenticada.

Classes acessórias

Classe Função
PQDAnnotation Parser de phpDOC usado pelos @field, @table, @list
PQDCrypt Encriptação simétrica para tokens e cookies
PQDLogs Logger de acessos e SQL com rotação por arquivo
PQDDynamicView / PQDDynamicReport Views/relatórios renderizados a partir de definição declarativa
PQDWorkflow Máquina de estados para entidades com fluxo de aprovação
DB\DB, DB\DBTable, DB\DBColumn Introspecção de schema (gerar migrações, comparar bancos)
DBF\Dbf, DbfToSQL, DbfToSQLite Importação de arquivos .dbf legados
LDAP\LDAPCon Cliente LDAP para autenticação corporativa
REPORT\Report Engine de relatórios paramétricos exportáveis em HTML/PDF/XLS

Estrutura de pastas sugerida para a aplicação

projeto/
├── composer.json
├── req_default.php          # bootstrap (require_once vendor/autoload + PQDApp::run)
├── index.php                # redireciona para public/
├── public/
│   ├── index.php            # require_once '../req_default.php'
│   └── .htaccess            # rewrite all → index.php
├── templates/
│   ├── tpl.head.php
│   └── tpl.footer.php
├── modulos/
│   ├── util/                # base classes da aplicação que estendem PQD*
│   └── <grupo>/<caso>/
├── scripts/                 # SQL inicial e migrações
├── logs/
├── uploads/
└── vendor/

Versão atual

1.4 — ver composer.json.