willkerms/pqd 问题修复 & 功能扩展

解决BUG、新增功能、兼容多环境部署,快速响应你的开发需求

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

willkerms/pqd

Composer 安装命令:

composer require willkerms/pqd

包简介

PHP Quick Development

README 文档

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.

willkerms/pqd 适用场景与选型建议

willkerms/pqd 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 464 次下载、GitHub Stars 达 3, 最近一次更新时间为 2017 年 05 月 02 日, 在 PHP 生态内属于活跃度较高的组件。

我们在过去多个企业项目中使用过 willkerms/pqd 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。

围绕 willkerms/pqd 我们能提供哪些服务?
定制开发 / 二次开发

基于 willkerms/pqd 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

统计信息

  • 总下载量: 464
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 3
  • 点击次数: 11
  • 依赖项目数: 2
  • 推荐数: 0

GitHub 信息

  • Stars: 3
  • Watchers: 1
  • Forks: 3
  • 开发语言: PHP

其他信息

  • 授权协议: GPL-2.0-or-later
  • 更新时间: 2017-05-02