Dados e segurança
Modelos e banco de dados
Definir um modelo
Cada tabela tem uma classe que estende TModel. O arquivo fica em src/Models/<nome>.php (ou plugins/<plugin>/src/Models/) e a classe se chama TModel + o nome em CamelCase.
<?php
// src/Models/productos.php
Koshkil::Uses('sys.db.model');
Koshkil::UsesModel('categorias');
class TModelProductos extends TModel {
protected $tableName = 'tbl_productos';
public $primaryKeyColumn = 'prd_codigo';
// Somente estes campos são atribuídos com create(), fill() e update()
protected $fillable = ['cat_codigo', 'prd_nombre', 'prd_precio', 'prd_destacado', 'prd_alta', 'prd_datos'];
protected $dates = ['prd_alta']; // exibidos como dd/mm/aaaa e gravados como aaaa-mm-dd
protected $json = ['prd_datos']; // decodificado para array na leitura
protected function setupTableStructure() {
$this->table->id('prd_codigo')
->varchar('prd_nombre', 120, false, '')
->decimal('prd_precio', 10, 2, false, 0)
->enum('prd_destacado', ['0', '1'], false, '0')
->datetime('prd_alta')
->text('prd_datos')
->belongsTo('categorias') // adiciona cat_codigo + chave estrangeira
->addKey('idx_prd_nombre', 'prd_nombre');
}
// Método próprio: usado como $producto->precioConIva()
public function precioConIva() {
return round($this->prd_precio * 1.21, 2);
}
}
Para usá-lo:
Koshkil::UsesModel('productos'); // inclui o arquivo e cria o apelido TMProductos
Koshkil::UsesModel('noticias.noticias'); // modelo de um plugin -> TMNoticias
UsesModel() procura primeiro em src/Models/ e depois em core/models/, então um projeto pode substituir um modelo do núcleo.
Propriedades do modelo
| Propriedade | Uso |
|---|---|
$tableName |
Nome da tabela. |
$primaryKeyColumn |
Chave primária (pública). |
$fillable |
Campos que podem ser atribuídos em massa. Os demais são ignorados ao gravar. |
$dates |
Campos de data: lidos como dd/mm/aaaa [hh:mm:ss] e gravados em formato SQL. |
$json |
Campos decodificados de JSON na leitura. |
$skipEncoding |
Campos que não passam por htmlentities() ao gravar (por exemplo HTML de um editor). |
$encodeQuotes |
Campos em que as aspas também são codificadas. |
$readOnly |
true desativa create, update e delete. |
$behaviors |
Comportamentos a carregar (veja abaixo). |
$dependantModels |
Modelos cujos registros dependem deste (usados por canDelete() e cascadeDelete()). |
$alias |
Apelido padrão da tabela nas consultas. |
Codificação ao gravar
Por padrão, os textos são gravados passando por htmlentities() (preservando <, >, & e aspas). Se um campo guarda HTML ou dados que não devem ser transformados, adicione-o a $skipEncoding.
O esquema
setupTableStructure() descreve a tabela com $this->table, um construtor encadeável.
Colunas
| Método | Assinatura |
|---|---|
id |
id($coluna) — inteiro autoincremental e chave primária |
integer |
integer($coluna, $tamanho, $null = true, $default = null) |
tinyint |
tinyint($coluna, $tamanho, $null = true, $default = null) |
decimal |
decimal($coluna, $tamanho, $decimais, $null = true, $default = null) |
float |
float($coluna, $tamanho, $decimais, $null = true, $default = null) |
double |
double($coluna, $null = true, $default = null) |
char / varchar |
varchar($coluna, $tamanho, $null = true, $default = null, $collate = null) |
text / longtext |
text($coluna, $null = true, $default = null) |
enum |
enum($coluna, $valores, $null = true, $default = null) |
date / datetime |
datetime($coluna, $null = true, $default = null) |
blob / longblob |
blob($coluna) |
Todos aceitam um último argumento $options (por exemplo ['extra' => 'AUTO_INCREMENT']).
Chaves, opções da tabela e dados iniciais
$this->table
->addKey('idx_email', 'usr_email')
->addKey('idx_composta', ['cat_codigo', 'prd_nombre'])
->engine('InnoDB')
->charset('utf8mb4')
->collate('utf8mb4_unicode_ci')
->initialRecords([
['cat_nombre' => 'Geral'],
['cat_nombre' => 'Ofertas'],
]);
initialRecords() insere esses registros quando a tabela é criada.
Sincronização automática
Com Database.AutoUpdateSchema = true, na primeira vez que um modelo é instanciado (e sempre que seu arquivo muda), Manager::checkTable() compara a definição com a tabela real:
- se a tabela não existe, cria-a com suas chaves e dados iniciais;
- adiciona as colunas novas e altera as que mudaram de tipo ou tamanho;
- cria as chaves e chaves estrangeiras que faltam (se a tabela referenciada ainda não existe, a chave fica pendente até que exista).
As colunas que não estão no modelo são apagadas
A sincronização executa DROP em toda coluna da tabela que não esteja em setupTableStructure(). Antes de apontar um modelo para uma tabela existente, declare todas as suas colunas. Em produção você pode desativar AutoUpdateSchema e aplicar as mudanças com migrações.
A estrutura resultante é guardada no cache models junto com a data do arquivo do modelo; enquanto o arquivo não mudar, o banco não é consultado de novo.
Criar, ler, atualizar e apagar
// Criar: retorna o registro criado (com sua chave primária)
$producto = TMProductos::create([
'prd_nombre' => 'Notebook 14"',
'prd_precio' => 850000,
'prd_alta' => date('d/m/Y H:i:s'),
]);
// Ler
$producto = TMProductos::find(15); // pela chave primária, ou null
$producto->prd_nombre; // como propriedade
$producto['prd_nombre']; // ou como array
$producto->record(); // todos os campos em um array
// Atualizar
$producto->fill(['prd_precio' => 799000])->update();
$producto->prd_destacado = '1';
$producto->update();
// Apagar
$producto->delete(); // respeita readOnly e comportamentos
if ($categoria->canDelete()) { // tem registros dependentes?
$categoria->cascadeDelete(); // apaga também os dependentes
}
// Operações em massa (uma única instrução, sem eventos)
TMProductos::updateAll(['prd_destacado' => '0'], ['cat_codigo' => 3]);
TMProductos::deleteAll(['cat_codigo' => [7, 8]]); // um array vira IN (…)
create() também aceita um segundo argumento com campos extras permitidos só naquela chamada.
Consultas
Chamadas estáticas a métodos que o modelo não tem são encaminhadas ao seu TQueryBuilder, então as consultas começam direto na classe:
$lista = TMProductos::where('prd_destacado', '1')
->where('prd_precio', '<', 100000)
->order('prd_nombre', 'ASC')
->take(12)
->get(); // TCollection de TMProductos
foreach ($lista as $producto) {
echo $producto->prd_nombre;
}
Referência do construtor de consultas
| Método | Exemplo |
|---|---|
select($campos) |
select('prd_codigo, prd_nombre') |
distinct() |
|
alias($a) |
TMProductos::alias('p') |
where(…) |
where('campo', 'valor'), where('campo', '>', 5), where('campo', ['a','b']) (IN), where('campo', 'in', [1,2]), where('SQL literal'), where(['campo', '>', 5]) (condição como array) |
orWhere(…) |
orWhere([['usr_email', $x], ['usr_user', $x]]) → (… OR …) |
whereNull($campo) |
|
rawWhere(…) |
condição sem processar (SQL literal) |
join($tabela, $condição, $tipo = 'inner') |
join(TMCategorias::alias('c'), 'c.cat_codigo = p.cat_codigo', 'left') |
joinUsing($tabela, $coluna) |
joinUsing(TMUsuariosRoles::alias('ur'), 'usr_codigo') |
group($campos) / having(…) / orHaving(…) |
|
order($campo, $dir) |
campo e direção validados; order('expressão SQL') com um argumento é literal |
take($n) / offset($n) |
limite e deslocamento |
pageSize($n)->page($p) |
paginação |
get() |
executa e retorna uma TCollection |
first() |
primeiro registro ou null |
find($id) |
pela chave primária |
getAsArray() |
arrays em vez de modelos |
lists('campo') |
array de valores; com vários campos, array de linhas |
each($callback) |
percorre os resultados |
dataTable() |
resposta no formato do jQuery DataTables |
compile() |
retorna o SQL sem executá-lo |
debug(true) |
registra o SQL |
noEvents() |
não dispara eventos do modelo |
Coleções
get() retorna uma TCollection: é percorrida com foreach, acessada por índice e oferece count(), first(), last(), map(), each(), slice() e getKeys(). A propriedade totalRecords tem o total de registros sem o limite (útil para paginar) e compiledSQL, a consulta executada.
$pagina = TMProductos::order('prd_nombre')->pageSize(20)->page(2)->get();
$paginas = ceil($pagina->totalRecords / 20);
Segurança nas consultas
insert e update usam instruções preparadas, e o construtor de consultas escapa automaticamente todos os valores recebidos por where(), orWhere(), having(), orHaving(), whereEncoding(), find(), lists(), updateAll() e deleteAll(). Os dados do usuário são passados como estão, sem escapá-los antes:
TMUsuarios::where('usr_email', $this->Request->email)->first(); // escapado pelo where()
TMProductos::where('prd_nombre', 'like', "%{$this->Request->q}%")->get();
TMProductos::where('cat_codigo', [$a, $b, $c])->get(); // cada elemento é escapado
Não escape duas vezes
Passar para where() um valor já escapado (Koshkil::escape(), $this->esc()) o escapa de novo, e as buscas com aspas ou barras deixam de coincidir.
Além disso, o construtor valida o que não pode escapar:
| Parte | Proteção |
|---|---|
Operador (where('campo', $op, $valor)) |
Só aceita =, <>, !=, <, >, <=, >=, <=>, LIKE, NOT LIKE, IN, NOT IN, IS, IS NOT, REGEXP, NOT REGEXP e RLIKE. Qualquer outro lança TQueryBuilderException. |
order($campo, $dir) |
O campo precisa ser um nome de coluna (coluna ou tabela.coluna) e a direção ASC ou DESC; caso contrário, lança TQueryBuilderException. Assim é possível ordenar por uma coluna escolhida na interface. |
take() / offset() |
São convertidos para inteiro. |
Continuam sendo SQL literal, e nunca devem receber dados do usuário sem tratamento: where('SQL literal') com um único argumento, rawWhere(), as condições de join(), select(), group() e order() com um único argumento. Se precisar de um valor dentro desses trechos, escape-o com Koshkil::escapeString() (sem aspas) ou Koshkil::quote() (com aspas), ou converta-o com intval():
$q = Koshkil::escapeString($this->Request->q);
$qb->where("MATCH(prd_nombre) AGAINST ('{$q}')");
$qb->where("FIND_IN_SET(" . intval($this->Request->grupo) . ", prd_grupos)");
$qb->where('p.cat_codigo = ' . Koshkil::quote($this->Request->categoria));
Dentro de um endpoint da API, $this->esc($valor) equivale a Koshkil::escapeString().
Relações
São declaradas em setupTableStructure() e geram métodos virtuais no modelo.
// Em TModelProductos: cada produto pertence a uma categoria
$this->table->belongsTo('categorias');
// cria a coluna cat_codigo (inteiro, chave primária de categorias), sua chave
// estrangeira e o método $producto->categoria() (nome do modelo no singular)
// Em TModelCategorias: uma categoria tem muitos produtos
$this->table->hasMany('productos', ['foreignKey' => 'cat_codigo', 'virtualMethodName' => 'productos', 'orderBy' => ['prd_nombre' => 'ASC']]);
// Um para um com condições extras
$this->table->hasOne('galeria', [
'foreignKey' => 'gal_relacionado',
'columnName' => 'usr_codigo',
'virtualMethodName' => 'imagenPerfil',
'where' => [['gal_grupo', 'usuarios'], ['gal_uso', 'profile']],
]);
| Opção | Relação | Significado |
|---|---|---|
column_name |
belongsTo |
Coluna local (por padrão, com o mesmo nome da chave primária do modelo relacionado). Serve para pertencer duas vezes ao mesmo modelo. |
columnName |
hasMany, hasOne |
Coluna local comparada (por padrão, a chave primária própria). |
foreignKey |
hasMany, hasOne |
Coluna do modelo relacionado que aponta para este. Convém informá-la sempre. |
virtualMethodName |
todas | Nome do método gerado. |
where |
hasMany, hasOne |
Condições adicionais. |
orderBy |
hasMany |
Ordem dos resultados: ['coluna' => 'ASC', …]. |
$categoria = TMCategorias::find(3);
foreach ($categoria->productos() as $producto) { … }
$foto = $usuario->imagenPerfil();
Eventos
Um método chamado <prefixo>_event_<evento> é registrado automaticamente como manipulador. O prefixo é livre (serve para agrupar, por exemplo pelo nome de um trait).
| Evento | Quando | Recebe / retorna |
|---|---|---|
setupTableStructure |
depois de definir o esquema | |
precreate |
antes de inserir | recebe e retorna o modelo a inserir |
create |
depois de inserir | recebe e retorna o registro criado |
update |
depois de atualizar | |
delete |
depois de apagar | |
getrecord |
ao construir cada item de uma coleção |
class TModelProductos extends TModel {
public function productos_event_precreate($record) {
$record->prd_alta = date('d/m/Y H:i:s');
return $record;
}
}
TMProductos::noEvents()->… desativa os eventos em uma consulta.
Traits incluídos
| Trait | Arquivo | Adiciona |
|---|---|---|
THierarchyTrait |
sys.db.traits.hierarchy |
Árvores pai-filho ($parentField, $textField): roots(), children(), getParent(), getRoot(), createAsChild(), makeRoot(), makeChildOf(). |
TOrderableTrait |
sys.db.traits.orderable |
Ordem manual ($orderField, padrão idx_order): indexUp(), indexDown(), setOrderIndex(), nextIndex(). |
TOwnableTrait |
sys.db.traits.ownable |
Atribui o usuário atual (usr_codigo) ao criar: belongsToUser(), strictlyBelongsToUser(), isMine(), owner(). |
TTranslatableTrait |
sys.db.traits.translatable |
Registros traduzidos por idioma (idi_codigo, $translationParent): translation($idioma), getTranslations(), mainLanguage(). |
TMultimediaTrait |
sys.db.traits.multimedia |
Arquivos associados na galeria: imagenes(), documentos(), videos(), sonidos(), multimedia(). |
Koshkil::uses('sys.db.traits.hierarchy');
Koshkil::uses('sys.db.traits.ownable');
class TModelCategorias extends TModel {
use THierarchyTrait, TOwnableTrait;
protected function setupTableStructure() {
$this->parentField = 'cat_parent'; // a coluna é adicionada pelo trait
$this->textField = 'cat_nombre';
$this->table->id('cat_codigo')->varchar('cat_nombre', 45, false, '');
}
}
Comportamentos
Um comportamento adiciona métodos a vários modelos sem herança. É uma classe <Nome>Behavior que estende Behavior, em src/Models/Behaviors/<Nome>.php. Em um plugin, a classe leva também o nome do plugin como prefixo (<Plugin><Nome>Behavior) e é carregada como '<plugin>.<nome>'. Seus métodos são chamados como se fossem do modelo, que ele recebe em $this->record. Se definir beforeDelete($modelo), ele é executado antes de cada delete().
<?php
// src/Models/Behaviors/Publicable.php
Koshkil::uses('sys.db.behavior');
class PublicableBehavior extends Behavior {
public function publicar() {
$this->record->fill(['pub_estado' => '1'])->update();
}
}
class TModelArticulos extends TModel {
protected $behaviors = ['publicable'];
}
TMArticulos::find(4)->publicar();
Migrações
src/Migrations/ guarda scripts versionados com métodos up() e down() para mudanças que o esquema declarativo não cobre (dados, tabelas sem modelo, mudanças delicadas em produção). O núcleo não inclui um executor: rode-os a partir de um script próprio.
<?php
// src/Migrations/20260425120000_create_personal_negocios_table.php
class Migration20260425120000 {
public function up() {
Koshkil::$db->execute("CREATE TABLE IF NOT EXISTS `tbl_personal_negocios` (…)");
}
public function down() {
Koshkil::$db->execute("DROP TABLE IF EXISTS `tbl_personal_negocios`");
}
}
Acesso direto ao banco
Koshkil::$db (ou Koshkil::getDatabase()) é o driver ativo:
$db = Koshkil::getDatabase();
$db->execute("UPDATE tbl_productos SET prd_precio = prd_precio * 1.1");
$linha = $db->getRow("SELECT COUNT(*) AS total FROM tbl_productos");
$res = $db->getRecords("SELECT * FROM tbl_productos WHERE cat_codigo = 3"); // ['data' => […], 'records' => N]
$db->performTransaction([$sql1, $sql2]);
$db->escape($texto); // com PDO retorna o valor já entre aspas
Koshkil::escapeString($texto); // igual com qualquer driver, sem aspas
Koshkil::quote($texto); // 'texto'
$db->lastInsertId();
O driver é escolhido com Database.master.Driver: MySQLi, PostgreSQL, SQLite, SQLServer ou Oracle. O Koshkil tenta primeiro a versão PDO e, se não estiver disponível, a nativa.