Cheatsheet CodeIgniter
Framework PHP leve e simples
CodeIgniter
Instalação e Estrutura
Instalação
// Criar projeto com Composer: composer create-project codeigniter4/appstarter meu-projeto cd meu-projeto // Iniciar servidor de desenvolvimento: php spark serve // Requisitos: // - PHP 8.1+ // - ext-intl, ext-mbstring // - Composer // Alternativa (download manual): // codeigniter.com/download
composer create-project cria o projeto com todas as dependências. php spark serve inicia o servidor de desenvolvimento na porta 8080. O CodeIgniter 4 requer PHP 8.1+ com extensões intl e mbstring. É o framework PHP mais leve — sem dependências obrigatórias além do core.
Autoloading e Namespaces
// app/Config/Autoload.php
public array $psr4 = [
APP_NAMESPACE => APPPATH,
'App' => APPPATH,
];
// Adicionar namespace custom:
public array $psr4 = [
'App' => APPPATH,
'Acme' => ROOTPATH . 'acme/src',
];
// Uso em controllers/models:
use App\Models\ProdutoModel;
use Acme\Pagamentos\Gateway;
// Classmap (ficheiros específicos):
public array $classmap = [
'MinhaClasse' => APPPATH . 'Libraries/MinhaClasse.php',
];O CodeIgniter 4 usa autoloading PSR-4 — cada namespace mapeia para um diretório. APP_NAMESPACE aponta para app/. Namespaces custom permitem organizar bibliotecas externas. classmap mapeia classes individuais. O autoloader é rápido e não requer composer dump-autoload para classes em app/.
BaseController
<?php
namespace App\Controllers;
use CodeIgniter\Controller;
class BaseController extends Controller
{
protected $helpers = ['url', 'form'];
protected $session;
public function initController(
$request, $response, $logger
) {
parent::initController(
$request, $response, $logger
);
$this->session = session();
}
}
// Todos os controllers estendem BaseController
class Produto extends BaseController
{
// $this->session já disponível
}BaseController é o pai de todos os controllers — ideal para carregar helpers, iniciar sessão e definir dados partilhados. initController() é o "construtor" do CI4 (não usar __construct). $helpers carrega helpers automaticamente em todos os controllers. Propriedades definidas aqui ficam disponíveis em toda a aplicação.
Estrutura de pastas
app/
Config/ // configurações
Controllers/ // controladores
Models/ // modelos
Views/ // templates
Database/
Migrations/ // migrações
Seeds/ // seeders
Filters/ // middleware
Libraries/ // bibliotecas custom
public/
index.php // front controller
assets/ // CSS, JS, imagens
writable/
logs/ // logs da aplicação
cache/ // cache
uploads/ // ficheiros enviadosA pasta app/ contém todo o código da aplicação (MVC). public/ é o único diretório público (front controller index.php). writable/ armazena logs, cache e uploads (fora do acesso web). Esta separação protege o código-fonte — apenas public/ é servido pelo web server.
Constantes e paths
// Constantes de caminho (app/Config/Paths.php):
APPPATH // app/
ROOTPATH // raiz do projeto
WRITEPATH // writable/
PUBLICPATH // public/
SYSTEMPATH // vendor/codeigniter4/framework/system/
// Uso:
$logFile = WRITEPATH . 'logs/app.log';
$uploadDir = PUBLICPATH . 'uploads/';
$configFile = APPPATH . 'Config/App.php';
// Constantes de ambiente:
ENVIRONMENT // 'development' ou 'production'
CI_DEBUG // true em development
// Verificar ambiente:
if (ENVIRONMENT === 'development') {
// código só em dev
}Constantes de caminho (APPPATH, WRITEPATH, PUBLICPATH) evitam caminhos relativos frágeis. ENVIRONMENT indica o modo atual (development/production). CI_DEBUG é true em development. Estas constantes são definidas em app/Config/Paths.php e disponíveis globalmente sem import.
Composer e dependências
// composer.json (dependências):
{
"require": {
"php": "^8.1",
"codeigniter4/framework": "^4.4"
},
"require-dev": {
"fakerphp/faker": "^1.23",
"codeigniter4/devkit": "^1.2",
"phpunit/phpunit": "^10.5"
}
}
// Instalar dependências:
composer install
// Adicionar pacote:
composer require dompdf/dompdf
// Atualizar framework:
composer update codeigniter4/framework
// Scripts:
composer test // executar testes
composer analyze // análise estáticaO CodeIgniter 4 é instalado via Composer — codeigniter4/framework é o core. codeigniter4/devkit adiciona ferramentas de desenvolvimento (debugbar, faker). composer require adiciona pacotes de terceiros. O framework é minimalista — a maioria das funcionalidades é nativa, sem pacotes extra.
Configuração (.env)
# Copiar template: cp env .env # .env (configurações de ambiente): CI_ENVIRONMENT = development app.baseURL = "http://localhost:8080/" app.forceGlobalSecureRequests = false database.default.hostname = localhost database.default.database = minha_db database.default.username = root database.default.password = database.default.DBDriver = MySQLi # Produção: # CI_ENVIRONMENT = production
O ficheiro .env sobrescreve configurações do app/Config/ — nunca commitar (está no .gitignore). CI_ENVIRONMENT controla debug bar e error reporting. Em produção, definir production desativa detalhes de erro. app.baseURL deve ser a URL raiz do projeto.
Config App.php
// app/Config/App.php public string $baseURL = 'http://localhost:8080/'; public string $indexPage = 'index.php'; public string $uriProtocol = 'REQUEST_URI'; public string $defaultLocale = 'pt'; public bool $negotiateLocale = false; public array $supportedLocales = ['pt', 'en']; public string $defaultTimezone = 'Europe/Lisbon'; // Session: public string $sessionDriver = 'CodeIgniter\Session\Handlers\FileHandler'; public string $sessionCookieName = 'ci_session'; public int $sessionExpiration = 7200; // Security: public bool $CSPEnabled = false;
app/Config/App.php contém configurações globais: baseURL, locale, timezone, sessão e segurança. defaultLocale define o idioma padrão. sessionExpiration em segundos (7200 = 2h). CSPEnabled ativa Content Security Policy. A maioria pode ser sobrescrita no .env.
Configuração de BD
// app/Config/Database.php
public array $default = [
'DSN' => '',
'hostname' => 'localhost',
'username' => 'root',
'password' => '',
'database' => 'minha_db',
'DBDriver' => 'MySQLi',
'DBPrefix' => '',
'pConnect' => false,
'DBDebug' => true,
'charset' => 'utf8mb4',
'DBCollat' => 'utf8mb4_general_ci',
];
// Drivers suportados:
// MySQLi, Postgre, SQLite3, SQLSRV, OCI8A configuração de BD fica em app/Config/Database.php ou no .env. DBDriver define o driver (MySQLi, Postgre, SQLite3, SQLSRV). charset utf8mb4 suporta emoji e caracteres especiais. DBDebug mostra erros SQL em desenvolvimento. Suporta múltiplas conexões com grupos diferentes.
Múltiplos ambientes
# .env (development): CI_ENVIRONMENT = development database.default.hostname = localhost database.default.database = minha_db_dev # .env.production: CI_ENVIRONMENT = production database.default.hostname = db.servidor.com database.default.database = minha_db_prod # Comportamento por ambiente: # development: # - Debug toolbar visível # - Erros detalhados # - CI_DEBUG = true # production: # - Erros genéricos (sem stack trace) # - Debug toolbar oculta # - CI_DEBUG = false
CI_ENVIRONMENT controla o comportamento da aplicação. Em development, a debug toolbar mostra queries, tempo e memória. Em production, erros são genéricos (sem expor stack traces). Pode usar ficheiros .env diferentes por ambiente. O CodeIgniter não tem .env.example — usa o ficheiro env como template.
Rotas e Controladores
Rotas básicas
// app/Config/Routes.php
use CodeIgniter\Router\RouteCollection;
$routes->get('/', 'Home::index');
$routes->get('produtos', 'Produto::listar');
$routes->post('produtos', 'Produto::criar');
$routes->get('produtos/(:num)', 'Produto::ver/$1');
$routes->put('produtos/(:num)', 'Produto::atualizar/$1');
$routes->delete('produtos/(:num)', 'Produto::eliminar/$1');
// Placeholders:
// (:num) - apenas números
// (:alpha) - apenas letras
// (:any) - qualquer caractere
// (:segment) - segmento URI (sem /)
// (:alphanum) - alfanuméricoRotas são definidas em app/Config/Routes.php com verbos HTTP (get, post, put, delete). Placeholders como (:num) capturam segmentos da URI e passam como argumentos ($1). O formato é 'Controller::metodo'. Rotas são avaliadas na ordem de definição.
Redirecionamento
// Redirecionar para URL:
return redirect()->to('/produtos');
// Para rota nomeada:
return redirect()->route('nome_rota');
// Voltar atrás:
return redirect()->back();
// Com flash messages:
return redirect()->back()
->with('sucesso', 'Guardado com sucesso!');
// Com input antigo (re-popular form):
return redirect()->back()
->withInput()
->with('erro', 'Dados inválidos');
// Código HTTP custom:
return redirect()->to('/login')->with('msg', 'Sessão expirada');
// Na view:
// <?= session()->getFlashdata('sucesso') ?>redirect() retorna resposta de redirecionamento. ->with() define flash messages (disponíveis só no próximo request). ->withInput() preserva input do formulário (via old() na view). ->route() usa nomes de rotas (mais seguro que URLs hardcoded). Flash messages são ideais para feedback pós-ação (PRG pattern).
Request e resposta
// No controller:
public function index()
{
// Request:
$method = $this->request->getMethod();
$uri = $this->request->getUri();
$ip = $this->request->getIPAddress();
$isAjax = $this->request->isAJAX();
$isSecure = $this->request->isSecure();
// Headers:
$auth = $this->request->getHeaderLine('Authorization');
// Resposta custom:
return $this->response
->setStatusCode(201)
->setHeader('X-Custom', 'valor')
->setBody($conteudo);
// Download:
return $this->response
->download('ficheiro.pdf', $dados);
}$this->request fornece acesso ao request HTTP: método, URI, IP, headers, AJAX. $this->response permite resposta custom com status code, headers e body. ->download() força download de ficheiro. isAJAX() deteta pedidos XMLHttpRequest. Estes objetos são injetados automaticamente no controller via initController().
Resource routes
// Rotas RESTful automáticas:
$routes->resource('produtos');
// Gera 7 rotas:
// GET /produtos -> index
// GET /produtos/new -> new
// POST /produtos -> create
// GET /produtos/(:num) -> show
// GET /produtos/(:num)/edit -> edit
// PUT /produtos/(:num) -> update
// DELETE /produtos/(:num) -> delete
// Presenter (formulários HTML):
$routes->presenter('produtos');
// Limitar métodos:
$routes->resource('produtos', [
'only' => ['index', 'show', 'create']
]);
// Controller diferente:
$routes->resource('produtos', [
'controller' => 'Api\ProdutoController'
]);$routes->resource() gera as 7 rotas RESTful automaticamente — ideal para CRUD completo. $routes->presenter() é variante para formulários HTML (usa new/edit em vez de JSON). only limita os métodos gerados. O controller deve estender ResourceController para API ou ResourcePresenter para HTML.
Rotas nomeadas
// Definir nome:
$routes->get('produtos/(:num)', 'Produto::ver/$1',
['as' => 'produto.ver']
);
$routes->get('perfil', 'Perfil::index',
['as' => 'perfil']
);
// Usar nome em redirecionamentos:
return redirect()->route('produto.ver', [$id]);
// Gerar URL na view:
<a href="<?= url_to('produto.ver', $p['id']) ?>">
Ver produto
</a>
<a href="<?= url_to('perfil') ?>">Meu Perfil</a>
// Vantagem: mudar a URL sem quebrar links
// (basta alterar a rota, nomes mantêm-se)Rotas nomeadas (['as' => 'nome']) desacoplam URLs de links. url_to() gera a URL a partir do nome e parâmetros. redirect()->route() redireciona por nome. Se a URL mudar, só altera a definição da rota — todos os links atualizam automaticamente. Essencial para manutenção e refatoração sem quebrar referências.
Error handling
// Lançar 404:
throw \CodeIgniter\Exceptions\PageNotFoundException
::forPageNotFound('Produto não encontrado');
// Erro custom:
throw new \RuntimeException('Erro no pagamento', 500);
// Páginas de erro custom:
// app/Views/errors/html/error_404.php
// app/Views/errors/html/error_exception.php
// app/Views/errors/html/production.php
// Handler global (app/Config/Exceptions.php):
// Personalizar resposta de erro
// Log de erros:
log_message('error', 'Falha no pagamento: ' . $e->getMessage());
// Em produção:
// Erros mostram página genérica (sem detalhes)PageNotFoundException retorna 404 com página custom (error_404.php). Em development, erros mostram stack trace completa; em production, página genérica. Páginas de erro ficam em app/Views/errors/html/. log_message() regista em writable/logs/. Nunca expor detalhes de erro em produção (segurança).
Controller básico
<?php
namespace App\Controllers;
class Produto extends BaseController
{
public function index()
{
$model = new \App\Models\ProdutoModel();
$data['produtos'] = $model->findAll();
return view('produtos/lista', $data);
}
public function ver($id)
{
$model = new \App\Models\ProdutoModel();
$data['produto'] = $model->find($id);
if (!$data['produto']) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
return view('produtos/detalhe', $data);
}
}Controllers estendem BaseController e contêm métodos de ação. return view() renderiza template com dados. PageNotFoundException retorna 404 automaticamente. O controller orquestra: recebe request, chama model, passa dados à view. Manter lógica de negócio nos models/services, não no controller.
Filtros de rota
// Filtro em rota individual:
$routes->get('admin', 'Admin::index',
['filter' => 'auth']
);
// Múltiplos filtros:
$routes->get('admin/relatorios', 'Admin::Relatorios',
['filter' => ['auth', 'admin']
);
// Filtro com parâmetros:
$routes->get('api/dados', 'Api::dados',
['filter' => 'throttle:60']
);
// Filtros globais (Config/Filters.php):
public array $globals = [
'before' => ['csrf', 'honeypot'],
'after' => ['toolbar', 'secureheaders'],
];
// Filtro por padrão URI:
public array $filters = [
'auth' => ['before' => ['admin/*']],
];Filtros são middleware do CI4 — executam antes (before) e/ou depois (after) do controller. Podem ser aplicados por rota, grupo, padrão URI ou globalmente. csrf e honeypot são built-in. Filtros custom implementam FilterInterface. Podem receber parâmetros (throttle:60). Ideais para autenticação, logging e rate limiting.
Route constraints
// Restringir por hostname:
$routes->get('docs', 'Docs::index',
['hostname' => 'docs.meusite.com']
);
// Restringir por subdomínio:
$routes->group('', ['hostname' => 'api.*'],
function ($routes) {
$routes->get('users', 'Api\Users::index');
}
);
// Regex custom no placeholder:
$routes->get('users/([a-z-]+)', 'Users::ver/$1');
// Prioridade de rotas:
$routes->setPrioritize();
// Rotas mais específicas primeiro
// Verificar rota atual:
$route = service('router')->getRouteName();
$controller = service('router')->controllerName();
$method = service('router')->methodName();Rotas podem ser restringidas por hostname (subdomínios, multi-site). Regex custom nos placeholders permite padrões complexos. setPrioritize() ativa prioridade por especificidade. service('router') fornece informação da rota atual (útil em layouts para menus ativos). Rotas são avaliadas na ordem — definir específicas antes de genéricas.
Grupos e namespaces
// Agrupar com prefixo:
$routes->group('admin',
['namespace' => 'App\Controllers\Admin'],
function ($routes) {
$routes->get('dashboard', 'Dashboard::index');
$routes->get('users', 'Users::index');
$routes->get('settings', 'Settings::index');
}
);
// URLs: /admin/dashboard, /admin/users
// Grupo com filtro (middleware):
$routes->group('api',
['filter' => 'auth:api'],
function ($routes) {
$routes->get('perfil', 'Api\Perfil::show');
}
);
// Sub-grupos:
$routes->group('admin', function ($routes) {
$routes->group('users', function ($routes) {
$routes->get('/', 'Admin\Users::index');
});
});$routes->group() agrupa rotas com prefixo URI, namespace e filtros partilhados. Evita repetição de prefixos e aplica middleware a múltiplas rotas. Sub-grupos permitem hierarquia (/admin/users). O namespace no grupo evita qualificar cada controller. Filtros no grupo aplicam-se a todas as rotas internas.
Auto-routing
// app/Config/Routes.php // Auto-routing (CI4.5+): $routes->setAutoRoute(true); // URL: /produtos/listar // -> App\Controllers\Produtos::listar() // URL: /admin/users/index // -> App\Controllers\Admin\Users::index() // Desativar (recomendado em produção): $routes->setAutoRoute(false); // Auto-routing com tradução de namespace: $routes->setAutoRoute(true); $routes->setTranslateURIDashes(true); // /meus-produtos -> MeusProdutos controller // Prioridade: rotas definidas > auto-route
Auto-routing mapeia URIs diretamente para controllers/métodos sem definição explícita. Convenção: /controller/metodo/params. setTranslateURIDashes converte hífens em CamelCase. Em produção, é recomendado desativar (setAutoRoute(false)) e definir rotas explicitamente — mais seguro e documentado. Rotas definidas têm prioridade sobre auto-routing.
Modelos e BD
Criar Model
<?php
namespace App\Models;
use CodeIgniter\Model;
class ProdutoModel extends Model
{
protected $table = 'produtos';
protected $primaryKey = 'id';
protected $allowedFields = ['nome', 'preco', 'categoria'];
protected $useTimestamps = true;
protected $createdField = 'created_at';
protected $updatedField = 'updated_at';
protected $returnType = 'array';
protected $useSoftDeletes = false;
}
// Criar via CLI:
// php spark make:model ProdutoO Model do CI4 configura-se via propriedades: $table, $primaryKey, $allowedFields (mass assignment seguro). $useTimestamps gere created_at/updated_at automaticamente. $returnType define se retorna array ou objeto. php spark make:model gera o ficheiro automaticamente.
Paginação
// No controller:
public function index()
{
$model = new ProdutoModel();
$data['produtos'] = $model
->orderBy('nome', 'ASC')
->paginate(10);
$data['pager'] = $model->pager;
return view('produtos/lista', $data);
}
// Na view:
<?php foreach ($produtos as $p): ?>
<p><?= esc($p['nome']) ?></p>
<?php endforeach; ?>
<?= $pager->links() ?>
// Links com grupo:
<?= $pager->links('grupo1') ?>
// Simple pagination (anterior/próximo):
<?= $pager->simpleLinks() ?>paginate(10) retorna 10 registos e configura o pager automaticamente. $model->pager fornece o objeto de paginação. $pager->links() renderiza links HTML (números, anterior, próximo). Lê a página da query string (?page=2). Suporta múltiplos grupos de paginação na mesma página. Integra-se com Query Builder.
Relações entre Models
// CI4 não tem Eloquent-style relations
// Usar joins ou queries no Model:
class PedidoModel extends Model
{
protected $table = 'pedidos';
public function comItens(int $pedidoId): array
{
return $this->db->table('pedido_itens')
->where('pedido_id', $pedidoId)
->get()
->getResultArray();
}
public function comCliente()
{
return $this->select('pedidos.*, clientes.nome')
->join('clientes',
'clientes.id = pedidos.cliente_id')
->findAll();
}
}
// Alternativa: Entity com métodos:
// $pedido->itens() retorna itens do pedidoO CI4 não tem relações Eloquent-style (hasMany, belongsTo). Relações implementam-se com join() ou métodos custom no Model que fazem queries à tabela relacionada. Entities podem ter métodos que carregam dados relacionados. É mais explícito e performante — sem lazy loading surpresa. Para relações complexas, considerar pacotes como codeigniter4-relations.
CRUD com Model
$model = new ProdutoModel();
// Criar:
$model->insert([
'nome' => 'TV Samsung',
'preco' => 500,
'categoria' => 'eletrónica'
]);
$id = $model->getInsertID();
// Ler:
$produto = $model->find($id);
$todos = $model->findAll();
$primeiros = $model->findAll(10); // limit
// Atualizar:
$model->update($id, ['preco' => 450]);
// Eliminar:
$model->delete($id);
// Verificar erros:
if (!$model->insert($dados)) {
$erros = $model->errors();
}Operações CRUD são métodos diretos: insert(), find(), findAll(), update(), delete(). getInsertID() retorna o ID gerado. errors() mostra erros de validação se falhar. $allowedFields protege contra mass assignment — só campos listados são inseridos/atualizados.
Query direta
$db = \Config\Database::connect();
// Query com binding (seguro):
$query = $db->query(
"SELECT * FROM produtos WHERE preco > ? AND categoria = ?",
[100, 'eletrónica']
);
// Resultados:
$resultados = $query->getResultArray(); // array
$objetos = $query->getResult(); // stdClass
// Uma linha:
$linha = $query->getRowArray();
// Named bindings:
$query = $db->query(
"SELECT * FROM users WHERE email = :email:",
['email' => $email]
);
// Query Builder sem Model:
$builder = $db->table('produtos');
$builder->where('ativo', 1)->get()->getResultArray();Database::connect() obtém a conexão. query() executa SQL direto com binding (? ou :nome:) — previne SQL injection. getResultArray() retorna arrays, getResult() objetos. Para queries complexas sem Model, use $db->table() como builder. Preferir Query Builder sempre que possível.
Transactions
$db = \Config\Database::connect();
// Transaction manual:
$db->transStart();
$db->table('pedidos')->insert($pedido);
$db->table('pedido_itens')->insertBatch($itens);
$db->table('produtos')
->where('id', $produtoId)
->set('stock', 'stock - 1', false)
->update();
$db->transComplete();
if ($db->transStatus() === false) {
// Rollback automático
log_message('error', 'Falha na transação');
}
// Transaction com try/catch:
try {
$db->transBegin();
// operações...
$db->transCommit();
} catch (\Exception $e) {
$db->transRollback();
throw $e;
}Transactions garantem atomicidade — todas as operações sucedem ou nenhuma. transStart()/transComplete() é o modo simples (rollback automático em falha). transBegin()/transCommit()/transRollback() dá controlo manual. Essencial para operações multi-tabela (pedidos + itens + stock). set('stock', 'stock - 1', false) evita escaping.
Query Builder
$model = new ProdutoModel();
// Condições encadeadas:
$resultados = $model
->where('preco >', 100)
->where('categoria', 'eletrónica')
->where('ativo', 1)
->orderBy('nome', 'ASC')
->limit(10)
->findAll();
// Like:
$model->like('nome', 'tv')
->orLike('descricao', 'televisão')
->findAll();
// Contar:
$total = $model->where('ativo', 1)
->countAllResults();
// whereIn:
$model->whereIn('id', [1, 2, 3])->findAll();
// whereNotIn:
$model->whereNotIn('categoria', ['removidos'])->findAll();O Query Builder permite consultas encadeadas sem SQL direto — protege contra SQL injection automaticamente. where(), like(), orderBy(), limit() compõem a query. countAllResults() conta sem retornar dados. whereIn()/whereNotIn() para listas de valores. Métodos são chainable e legíveis.
Soft deletes
// No Model:
class ProdutoModel extends Model
{
protected $useSoftDeletes = true;
protected $deletedField = 'deleted_at';
}
// Eliminar (soft - marca deleted_at):
$model->delete($id);
// Incluir eliminados:
$model->withDeleted()->findAll();
// Só eliminados:
$model->onlyDeleted()->findAll();
// Eliminar permanentemente:
$model->delete($id, true);
// Restaurar:
$model->update($id, ['deleted_at' => null]);
// Na migration (campo necessário):
'deleted_at' => [
'type' => 'DATETIME',
'null' => true,
]Soft deletes ($useSoftDeletes = true) marcam registos com deleted_at em vez de remover. delete() faz soft delete; delete($id, true) remove permanentemente. withDeleted() inclui eliminados nas queries. onlyDeleted() mostra só eliminados. Essencial para auditoria e recuperação de dados. Requer campo deleted_at na tabela.
Batch operations
$model = new ProdutoModel();
// Insert múltiplo:
$dados = [
['nome' => 'TV', 'preco' => 500],
['nome' => 'Rádio', 'preco' => 80],
['nome' => 'PC', 'preco' => 1200],
];
$model->insertBatch($dados);
// Update múltiplo:
$model->updateBatch($dados, 'nome');
// Atualiza onde 'nome' coincide
// Delete múltiplo:
$model->whereIn('id', [1, 2, 3])->delete();
// Chunk (processar em lotes):
$model->chunk(100, function ($row) {
// Processar cada linha
log_message('debug', $row['nome']);
});
// Contar total:
$total = $model->countAll();insertBatch() insere múltiplas linhas numa query (muito mais rápido que loop de insert()). updateBatch() atualiza por chave de coincidência. chunk() processa registos em lotes sem carregar tudo em memória — essencial para milhares de registos. countAll() conta sem retornar dados. Operações batch são O(1) queries vs O(n).
Select e Join
// Select específico:
$model->select('id, nome, preco')
->findAll();
// Select com alias:
$model->select('produtos.*, categorias.nome as cat')
->join('categorias',
'categorias.id = produtos.categoria_id')
->findAll();
// Join com tipo:
$model->select('p.*, c.nome as cliente')
->join('clientes c', 'c.id = p.cliente_id', 'left')
->findAll();
// Múltiplos joins:
$model->select('p.*, c.nome as cat, f.nome as forn')
->join('categorias c', 'c.id = p.categoria_id')
->join('fornecedores f', 'f.id = p.fornecedor_id')
->findAll();
// groupBy / having:
$model->select('categoria, COUNT(*) as total')
->groupBy('categoria')
->having('total >', 5)
->findAll();select() limita colunas retornadas (evitar SELECT * em produção). join() aceita tabela, condição e tipo (inner, left, right). Múltiplos joins encadeiam-se. groupBy() e having() para agregações. Usar alias (as cat) para evitar conflitos de nomes entre tabelas.
Callbacks e eventos
class ProdutoModel extends Model
{
protected $beforeInsert = ['hashSlug'];
protected $afterFind = ['formatarPreco'];
protected $beforeUpdate = ['atualizarSlug'];
protected function hashSlug(array $data)
{
$data['data']['slug'] = url_title(
$data['data']['nome'], '-', true
);
return $data;
}
protected function formatarPreco(array $data)
{
if (isset($data['data']['preco'])) {
$data['data']['preco_fmt'] =
number_format($data['data']['preco'], 2);
}
return $data;
}
}
// Callbacks disponíveis:
// beforeInsert, afterInsert
// beforeUpdate, afterUpdate
// beforeFind, afterFind
// beforeDelete, afterDeleteCallbacks executam automaticamente antes/depois de operações do Model. $beforeInsert modifica dados antes de inserir (ex: gerar slug). $afterFind formata resultados (ex: formatar preço). Recebem e retornam $data (array com chave 'data'). Ideais para lógica transversal sem poluir controllers.
Entities
// app/Entities/Produto.php
namespace App\Entities;
use CodeIgniter\Entity\Entity;
class Produto extends Entity
{
protected $casts = [
'preco' => 'float',
'ativo' => 'boolean',
'tags' => 'json-array',
];
// Mutator (setter):
public function setNome(string $val): self
{
$this->attributes['nome'] = ucfirst($val);
$this->attributes['slug'] = url_title($val, '-', true);
return $this;
}
// Accessor (getter):
public function getPrecoFormatado(): string
{
return number_format($this->preco, 2) . ' €';
}
}
// No Model:
protected $returnType = \App\Entities\Produto::class;
// Uso:
$produto = $model->find(1);
echo $produto->preco_formatado;Entities são objetos ricos que representam registos da BD. $casts converte tipos automaticamente (float, boolean, json-array). Mutators (setCampo) transformam ao definir; accessors (getCampo) ao ler. $returnType no Model faz find() retornar Entity em vez de array. Ideais para lógica de domínio e formatação.
Views e Layouts
View simples
// Controller:
public function index()
{
$data = [
'titulo' => 'Meus Produtos',
'produtos' => $model->findAll(),
];
return view('produtos/lista', $data);
}
// app/Views/produtos/lista.php:
<h1><?= esc($titulo) ?></h1>
<?php if (empty($produtos)): ?>
<p>Sem produtos.</p>
<?php else: ?>
<?php foreach ($produtos as $p): ?>
<div>
<h3><?= esc($p['nome']) ?></h3>
<p><?= esc($p['preco']) ?> €</p>
</div>
<?php endforeach; ?>
<?php endif; ?>view('pasta/ficheiro', $data) renderiza template com dados extraídos como variáveis. Sempre usar esc() no output para prevenir XSS. Templates são PHP puro com sintaxe alternativa (foreach:/endforeach;). Views ficam em app/Views/ organizadas por subdiretórios. Não colocar lógica de negócio em views.
esc() e segurança
// SEMPRE escapar output: <?= esc($nome) ?> <?= esc($html, 'html') ?> <?= esc($url, 'url') ?> <?= esc($js, 'js') ?> <?= esc($attr, 'attr') ?> // Contextos: // 'html' - conteúdo HTML (default) // 'url' - URLs // 'js' - JavaScript // 'attr' - atributos HTML // 'css' - CSS // CSRF em formulários: <?= csrf_field() ?> // Gera: <input type="hidden" name="csrf_token" ...> // Meta tag (para AJAX): <meta name="csrf-token" content="<?= csrf_hash() ?>"> // NUNCA: // <?= $input_do_utilizador ?> ← XSS!
esc() é a defesa contra XSS — escapa output conforme o contexto (html, url, js, attr, css). Usar SEMPRE em dados do utilizador. csrf_field() gera token CSRF em formulários. csrf_hash() para meta tags (AJAX). Sem esc(), a aplicação é vulnerável a injeção de script. Regra: todo output dinâmico passa por esc().
Filters de output
// app/Config/Filters.php
// Filtro de output (after):
public array $globals = [
'after' => ['minify'],
];
// app/Filters/MinifyFilter.php
class MinifyFilter implements FilterInterface
{
public function before(RequestInterface $request, $arguments = null) {}
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
{
$body = $response->getBody();
$body = preg_replace('/\s+/', ' ', $body);
$response->setBody($body);
return $response;
}
}
// Filtro de segurança (headers):
$response->setHeader('X-Content-Type-Options', 'nosniff');
$response->setHeader('X-Frame-Options', 'DENY');Filtros after processam a resposta antes de enviar ao browser — ideais para minificar HTML, adicionar headers de segurança, comprimir output. FilterInterface tem before() e after(). Filtros globais aplicam-se a todas as rotas. Podem modificar body, headers e status code. Úteis para CSP, compressão e logging de respostas.
Layouts e secções
// Layout: app/Views/layouts/main.php
<!DOCTYPE html>
<html>
<head>
<title><?= $this->renderSection('titulo') ?></title>
</head>
<body>
<?= $this->include('partials/nav') ?>
<main>
<?= $this->renderSection('conteudo') ?>
</main>
<?= $this->renderSection('scripts') ?>
</body>
</html>
// Página:
<?= $this->extend('layouts/main') ?>
<?= $this->section('titulo') ?>Produtos<?= $this->endSection() ?>
<?= $this->section('conteudo') ?>
<h1>Lista de Produtos</h1>
<?= $this->endSection() ?>
<?= $this->section('scripts') ?>
<script src="/js/produtos.js"></script>
<?= $this->endSection() ?>Template inheritance com extend() e section()/endSection(). O layout usa renderSection() como placeholders. Cada página define o conteúdo de cada secção. Suporta múltiplas secções (titulo, conteudo, scripts). Elimina repetição de HTML (header, footer, nav). Similar ao Blade do Laravel mas com sintaxe PHP pura.
old() e validação na view
// Repopular formulário após erro:
<input type="text" name="nome"
value="<?= old('nome') ?>">
<textarea name="descricao">
<?= old('descricao') ?>
</textarea>
<select name="categoria">
<option value="1"
<?= old('categoria') == '1' ? 'selected' : '' ?>>
Eletrónica
</option>
</select>
// Mostrar erros de validação:
<?php if (session('erro_nome')): ?>
<span class="erro"><?= session('erro_nome') ?></span>
<?php endif; ?>
// Todos os erros:
<?php if (session('erros')): ?>
<ul>
<?php foreach (session('erros') as $e): ?>
<li><?= esc($e) ?></li>
<?php endforeach; ?>
</ul>
<?php endif; ?>old('campo') retorna o valor submetido anteriormente (flash) — repopula formulários após erro de validação. Erros são passados via session flash. session('erro_campo') mostra erro individual. Combinado com redirect()->back()->withInput() no controller. Essencial para UX — utilizador não perde dados ao submeter.
Data sharing entre views
// Partilhar dados com todas as views:
// No BaseController:
public function initController($request, $response, $logger)
{
parent::initController($request, $response, $logger);
// Disponível em todas as views:
$this->data['user'] = session('user');
$this->data['app_name'] = 'Minha App';
}
// Renderer global:
$view = \Config\Services::renderer();
$view->setData([
'site_name' => 'Minha Loja',
'ano' => date('Y'),
]);
// Na view:
<footer>
<?= esc($site_name) ?> © <?= $ano ?>
</footer>
// View composer (dados por view):
$view->setData(['menu' => $this->getMenu()], 'layout');Dados partilhados evitam passar as mesmas variáveis a cada view. setData() no renderer torna dados globais. No BaseController, definir $this->data com info do utilizador/sessão. View composers permitem dados específicos por template. Reduz repetição e centraliza dados comuns (user, menu, configurações).
Partials (include)
// Incluir fragmento:
<?= $this->include('partials/header') ?>
<?= $this->include('partials/nav') ?>
<main>
<?= $conteudo ?>
</main>
<?= $this->include('partials/footer') ?>
// Include com dados:
<?= $this->include('partials/card', ['produto' => $p]) ?>
// Include com opções:
<?= $this->include('partials/alert', [
'tipo' => 'sucesso',
'msg' => 'Guardado!'
], ['saveData' => false]) ?>
// partials/alert.php:
<div class="alert alert-<?= $tipo ?>">
<?= esc($msg) ?>
</div>$this->include() insere fragmentos de view reutilizáveis (header, footer, cards, alerts). Aceita dados como segundo argumento. Terceiro argumento são opções (saveData, cache). Partials ficam em app/Views/partials/. Combinados com layouts, eliminam toda a repetição de HTML. Mais simples que componentes de frameworks JS.
View cells
// app/Cells/MenuCell.php
namespace App\Cells;
use CodeIgniter\View\Cells\Cell;
class MenuCell extends Cell
{
protected array $links;
public function mount(): void
{
$this->links = [
['url' => '/', 'label' => 'Início'],
['url' => '/produtos', 'label' => 'Produtos'],
];
}
}
// View: app/Cells/menu_view.php
<nav>
<?php foreach ($links as $link): ?>
<a href="<?= base_url($link['url']) ?>">
<?= esc($link['label']) ?>
</a>
<?php endforeach; ?>
</nav>
// Uso em qualquer view:
<?= view_cell('App\Cells\MenuCell') ?>View Cells são mini-controllers para views — encapsulam lógica e dados de componentes reutilizáveis. mount() carrega dados (como um controller). A view da cell é renderizada isoladamente. view_cell() invoca em qualquer template. Ideais para menus, sidebars, widgets que precisam de dados da BD. Substituem include + query manual.
Assets e URLs
// Estrutura recomendada:
// public/assets/css/style.css
// public/assets/js/app.js
// public/assets/img/logo.png
// Na view:
<link rel="stylesheet"
href="<?= base_url('assets/css/style.css') ?>">
<script
src="<?= base_url('assets/js/app.js') ?>"></script>
<img src="<?= base_url('assets/img/logo.png') ?>"
alt="Logo">
// Com versionamento (cache busting):
<link rel="stylesheet"
href="<?= base_url('assets/css/style.css?v=' . filemtime(PUBLICPATH . 'assets/css/style.css')) ?>">
// Favicon:
<link rel="icon"
href="<?= base_url('favicon.ico') ?>">
// NUNCA usar caminhos relativos:
// <link href="/css/style.css"> ← quebrar em subdirsAssets ficam em public/assets/ (acessíveis pelo browser). Sempre usar base_url() para gerar URLs — funciona em subdiretórios e domínios diferentes. Cache busting com filemtime() ou hash no nome do ficheiro. Nunca caminhos relativos (/css/) — quebram se a app não estiver na raiz. Organizar por tipo (css, js, img).
Helpers de URL e Form
// Carregar helpers:
helper('url');
helper(['form', 'html']);
// URL helpers:
base_url() // http://localhost:8080/
base_url('produtos') // .../produtos
site_url('admin/painel') // com index.php se configurado
current_url() // URL atual
previous_url() // URL anterior
uri_string() // segmento URI
// Form helpers:
echo form_open('produtos/salvar');
echo form_input('nome', old('nome'));
echo form_textarea('descricao');
echo form_dropdown('cat', $opcoes, $selected);
echo form_submit('', 'Guardar');
echo form_close();
// anchor:
echo anchor('produtos/1', 'Ver', ['class' => 'btn']);Helpers são funções auxiliares carregadas com helper(). base_url() gera URLs absolutas. form_open() cria <form> com CSRF token automático. form_input(), form_dropdown() geram campos HTML. anchor() cria links com atributos. Helpers de form integram com validação via old().
Caching de views
// Cache de view (em segundos):
return view('produtos/lista', $data, ['cache' => 300]);
// Cache com chave custom:
return view('pagina', $data, [
'cache' => 600,
'cache_name' => 'pagina_home'
]);
// Invalidar cache:
$cache = \Config\Services::cache();
$cache->delete('pagina_home');
// Cache de página inteira (no controller):
public function index()
{
// Cache por 5 minutos:
$this->cachePage(300);
return view('home');
}
// Configurar driver de cache:
// app/Config/Cache.php
public string $handler = 'file';
// Opções: file, memcached, redis, predisViews podem ser cacheadas com a opção ['cache' => segundos] — evita re-renderização. cachePage() cacheia a resposta inteira (ideal para páginas estáticas). cache_name permite invalidação manual. Drivers: file (default), memcached, redis. Cache de views é por template+dados — mudanças nos dados invalidam automaticamente.
Validação e Formulários
Regras no Controller
public function store()
{
$regras = [
'nome' => 'required|min_length[3]|max_length[100]',
'email' => 'required|valid_email|is_unique[users.email]',
'preco' => 'required|numeric|greater_than[0]',
];
if (!$this->validate($regras)) {
return redirect()->back()
->withInput()
->with('erros', $this->validator->getErrors());
}
// Dados válidos:
$dados = $this->request->getPost();
$model->insert($dados);
return redirect()->to('/produtos')
->with('sucesso', 'Criado!');
}$this->validate() verifica o input contra regras pipe-separated. Se falhar, getErrors() retorna mensagens. redirect()->back()->withInput() preserva dados do formulário. Regras são strings com | como separador. is_unique verifica unicidade na BD. Sempre validar antes de inserir/atualizar.
Upload de ficheiros
$ficheiro = $this->request->getFile('imagem');
if ($ficheiro->isValid() && !$ficheiro->hasMoved()) {
// Nome aleatório (seguro):
$nome = $ficheiro->getRandomName();
// Mover para pasta:
$ficheiro->move(WRITEPATH . '../public/uploads', $nome);
// Informações:
$original = $ficheiro->getClientName();
$ext = $ficheiro->getExtension();
$size = $ficheiro->getSize(); // bytes
$mime = $ficheiro->getMimeType();
}
// Validação de upload:
$regras = [
'imagem' => [
'label' => 'Imagem',
'rules' => 'uploaded[imagem]|max_size[imagem,2048]|is_image[imagem]|mime_in[imagem,image/jpg,image/png]',
],
];getFile() obtém o ficheiro enviado. isValid() verifica sucesso do upload. getRandomName() gera nome seguro (evita path traversal). move() transfere para destino. Validação com uploaded, max_size (KB), is_image, mime_in. Sempre validar tipo e tamanho. Nunca usar nome original diretamente.
Sanitização de input
// Filtros PHP nativos:
$email = filter_var($input, FILTER_SANITIZE_EMAIL);
$url = filter_var($input, FILTER_SANITIZE_URL);
$int = filter_var($input, FILTER_SANITIZE_NUMBER_INT);
// No request do CI4:
$email = $this->request->getPost('email', FILTER_SANITIZE_EMAIL);
// Limpar HTML:
$limpo = strip_tags($input);
$seguro = htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
// Trim e normalizar:
$nome = trim($this->request->getPost('nome'));
$nome = preg_replace('/\s+/', ' ', $nome);
// Escapar para contexto:
esc($dado, 'html'); // output HTML
esc($dado, 'url'); // URLs
esc($dado, 'js'); // JavaScript
esc($dado, 'attr'); // atributos
// Regra: validar > sanitizar > escaparSanitização limpa input antes de processar: FILTER_SANITIZE_EMAIL remove caracteres inválidos, strip_tags() remove HTML, trim() remove espaços. esc() escapa no output (não no input). Estratégia: validar (rejeitar inválido), sanitizar (limpar), escapar no output. Nunca confiar em sanitização como substituto de validação.
Regras no Model
class ProdutoModel extends Model
{
protected $validationRules = [
'nome' => 'required|min_length[3]|max_length[200]',
'preco' => 'required|numeric|greater_than[0]',
'email' => 'permit_empty|valid_email',
];
protected $validationMessages = [
'nome' => [
'required' => 'O nome é obrigatório',
'min_length' => 'Mínimo 3 caracteres',
],
'preco' => [
'required' => 'Indique o preço',
'numeric' => 'Deve ser numérico',
],
];
}
// Validação automática em insert/update:
if (!$model->insert($dados)) {
$erros = $model->errors();
}Regras no Model ($validationRules) validam automaticamente em insert()/update(). $validationMessages personaliza mensagens por campo/regra. Se a validação falhar, a operação é abortada e errors() retorna os erros. Centraliza validação no Model — controllers ficam mais limpos. permit_empty permite vazio mas valida se preenchido.
Mensagens custom
// Mensagens por campo e regra:
$regras = [
'nome' => [
'label' => 'Nome do Produto',
'rules' => 'required|min_length[3]',
'errors' => [
'required' => '{field} é obrigatório',
'min_length' => '{field} deve ter pelo menos {param} caracteres',
],
],
'email' => [
'label' => 'Email',
'rules' => 'required|valid_email|is_unique[users.email]',
'errors' => [
'is_unique' => 'Este {field} já está registado',
],
],
];
if (!$this->validate($regras)) {
$erros = $this->validator->getErrors();
}
// Placeholders: {field}, {param}, {value}Mensagens custom usam array com label, rules e errors. Placeholders: {field} (nome do campo), {param} (parâmetro da regra), {value} (valor submetido). label substitui o nome técnico por texto legível. Essencial para UX — mensagens claras ajudam o utilizador a corrigir erros.
Validação condicional
// Regras condicionais (if/else no controller):
$regras = ['tipo' => 'required|in_list[fisica,juridica]'];
if ($this->request->getPost('tipo') === 'fisica') {
$regras['cpf'] = 'required|exact_length[11]|numeric';
} else {
$regras['cnpj'] = 'required|exact_length[14]|numeric';
}
// required_if (CI 4.4+):
$regras = [
'pais' => 'required',
'estado' => 'required_if[pais,Brasil]',
'nif' => 'required_if[tipo,juridica]',
];
// required_with / required_without:
$regras = [
'telefone' => 'required_with[email]',
'fax' => 'required_without[telefone]',
];
// Permitir campos opcionais:
$regras = [
'website' => 'permit_empty|valid_url',
];Validação condicional aplica regras diferentes conforme o input. required_if[campo,valor] exige campo se outro tiver valor específico. required_with/required_without dependem de presença de outros campos. permit_empty permite vazio mas valida se preenchido. Para lógica complexa, construir regras dinamicamente no controller com if/else.
Regras disponíveis
// Presença: required, permit_empty // Tamanho: min_length[n], max_length[n], exact_length[n] // Tipo: alpha, alpha_numeric, alpha_dash, alpha_numeric_space numeric, integer, decimal // Formato: valid_email, valid_url, valid_ip, valid_json valid_date, valid_cc_num // Comparação: matches[campo], differs[campo] greater_than[n], less_than[n] greater_than_equal_to[n], less_than_equal_to[n] // BD: is_unique[tabela.campo] is_not_unique[tabela.campo] // Outros: in_list[a,b,c], regex_match[/pattern/] uploaded[campo], max_size[campo,n]
Regras cobrem presença (required), tamanho (min_length), tipo (numeric, valid_email), comparação (matches, greater_than) e BD (is_unique). in_list valida contra lista de valores. regex_match para padrões custom. uploaded/max_size para ficheiros. Regras encadeiam com |.
Regras custom
// app/Validation/RegrasCustom.php
namespace App\Validation;
class RegrasCustom
{
public function maior_que_zero(
?string $valor, string &$erro = null
): bool {
if ((float) $valor <= 0) {
$erro = 'O valor deve ser positivo';
return false;
}
return true;
}
public function sem_palavroes(
?string $valor, string &$erro = null
): bool {
$banidas = ['spam', 'scam'];
foreach ($banidas as $p) {
if (str_contains(strtolower($valor), $p)) {
$erro = "Contém palavra proibida: $p";
return false;
}
}
return true;
}
}
// Registar em Config/Validation.php:
public array $ruleSets = [
\App\Validation\RegrasCustom::class,
];
// Uso: 'preco' => 'required|maior_que_zero'Regras custom são métodos em classes registadas em Config/Validation.php. Recebem o valor e referência a $erro (mensagem). Retornam true/false. O nome do método é o nome da regra. Permitem validação de lógica de negócio específica. Registar no array $ruleSets. Usar como qualquer regra built-in.
Receber input
// POST:
$nome = $this->request->getPost('nome');
$todos = $this->request->getPost();
// GET / query string:
$q = $this->request->getGet('q');
$pagina = $this->request->getGet('page');
// Qualquer método (POST ou GET):
$valor = $this->request->getVar('campo');
// JSON body:
$data = $this->request->getJSON(true); // assoc
// Com filtro PHP:
$email = $this->request->getPost('email', FILTER_SANITIZE_EMAIL);
$idade = $this->request->getPost('idade', FILTER_VALIDATE_INT);
// Múltiplos valores (checkboxes):
$cores = $this->request->getPost('cores'); // array
// Verificar existência:
if ($this->request->getPost('nome') !== null) { }getPost() acede a dados POST, getGet() a query string, getVar() a qualquer método. getJSON(true) parseia body JSON como array associativo. Filtros PHP (FILTER_SANITIZE_EMAIL) sanitizam input. Nunca confiar em input do utilizador — sempre validar e escapar. getPost() sem argumento retorna todos os dados.
Validação de arrays
// Validar campos de array (formulários dinâmicos):
$regras = [
'itens.*.nome' => 'required|min_length[2]',
'itens.*.quantidade' => 'required|integer|greater_than[0]',
'itens.*.preco' => 'required|numeric',
];
// HTML:
// <input name="itens[0][nome]">
// <input name="itens[0][quantidade]">
// <input name="itens[1][nome]">
// Validar array simples:
$regras = [
'cores' => 'required',
'cores.*' => 'alpha|max_length[20]',
];
// Mensagens para arrays:
$mensagens = [
'itens.*.nome' => [
'required' => 'Nome do item é obrigatório',
],
];
// Erros incluem índice:
// "itens.0.nome" => "Nome do item é obrigatório"Wildcard * valida cada elemento de arrays — ideal para formulários dinâmicos (múltiplos itens). itens.*.nome aplica a regra a todos os elementos. Erros incluem o índice (itens.0.nome). Funciona com arrays simples (cores.*) e aninhados. Essencial para formulários de linhas repetidas (faturas, carrinhos).
Segurança e Sessões
CSRF Protection
// Ativar globalmente (Config/Filters.php):
public array $globals = [
'before' => ['csrf'],
];
// Configurar (Config/Security.php):
public string $tokenName = 'csrf_token';
public string $headerName = 'X-CSRF-TOKEN';
public int $expires = 7200;
public bool $regenerate = true;
// No formulário:
<?= csrf_field() ?>
// Em AJAX (meta tag no layout):
<meta name="csrf-token" content="<?= csrf_hash() ?>">
<meta name="csrf-header" content="X-CSRF-TOKEN">
// jQuery:
$.ajaxSetup({
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
}
});CSRF protection é ativado globalmente via filtro before. csrf_field() gera token hidden em formulários. Para AJAX, usar meta tag + header X-CSRF-TOKEN. regenerate cria novo token a cada request (mais seguro). expires define validade em segundos. Sem CSRF, formulários são vulneráveis a cross-site request forgery.
Autenticação simples
// Controller de login:
public function login()
{
$email = $this->request->getPost('email');
$pass = $this->request->getPost('password');
$model = new UserModel();
$user = $model->where('email', $email)->first();
if ($user && password_verify($pass, $user['password'])) {
session()->set([
'user_id' => $user['id'],
'nome' => $user['nome'],
'logged_in' => true,
]);
return redirect()->to('/dashboard');
}
return redirect()->back()
->with('erro', 'Credenciais inválidas');
}
// Logout:
public function logout()
{
session()->destroy();
return redirect()->to('/login');
}
// Verificar em filtro:
if (!session('logged_in')) {
return redirect()->to('/login');
}Autenticação básica: verificar credenciais com password_verify(), guardar estado na sessão. session()->destroy() no logout. Filtro auth protege rotas. Nunca guardar password em sessão. Para produção, considerar pacotes como codeigniter4/shield (autenticação oficial do CI4) com remember-me, throttle e verificação de email.
Proteção de dados sensíveis
// Nunca expor em logs:
log_message('error', 'Login falhou para: ' . $email);
// NUNCA: log_message('debug', 'Password: ' . $pass);
// .env fora do git:
// .gitignore:
.env
*.pem
*.key
// Não expor em erros:
// production: mensagem genérica
// development: stack trace (só local)
// Headers de segurança:
$response->removeHeader('X-Powered-By');
$response->removeHeader('Server');
// Validar acesso a recursos:
public function download($id)
{
$ficheiro = $model->find($id);
// Verificar dono:
if ($ficheiro['user_id'] !== session('user_id')) {
throw PageNotFoundException::forPageNotFound();
}
return $this->response->download($path, null);
}Nunca logar passwords, tokens ou dados sensíveis. .env e chaves fora do git. Em produção, erros são genéricos (sem stack trace). Remover headers que revelam tecnologia (X-Powered-By). Validar autorização em cada acesso a recurso (IDOR — Insecure Direct Object Reference). Verificar sempre se o utilizador é dono do recurso antes de retornar dados.
Sessões
$session = session();
// Definir:
$session->set('user_id', 42);
$session->set(['nome' => 'Ana', 'role' => 'admin']);
// Ler:
$id = $session->get('user_id');
$nome = session('nome'); // helper
// Verificar:
if ($session->has('user_id')) { }
// Remover:
$session->remove('temp');
// Destruir tudo:
$session->destroy();
// Flash (uma leitura):
$session->setFlashdata('msg', 'Sucesso!');
$msg = $session->getFlashdata('msg');
// Tempdata (expira em N segundos):
$session->setTempdata('codigo', $code, 300);Sessões do CI4 são acedidas via session() ou $this->session. set()/get() para dados persistentes. setFlashdata() para mensagens de uma leitura (redirect). setTempdata() expira automaticamente. Configuração em Config/App.php (driver, cookie, expiração). Driver default: file. Alternativas: database, redis, memcached.
Honeypot e anti-spam
// Ativar honeypot (Config/Filters.php):
public array $globals = [
'before' => ['honeypot'],
'after' => ['honeypot'],
];
// Configurar (Config/Honeypot.php):
public bool $hidden = true;
public string $label = 'Preencha este campo';
public string $name = 'website';
public string $template = '<label>{label}</label><input type="text" name="{name}" value="">';
// Como funciona:
// 1. Campo invisível adicionado ao form
// 2. Bots preenchem (não veem CSS)
// 3. Se preenchido -> request rejeitado
// Rate limiting manual:
$throttle = \Config\Services::throttler();
if ($throttle->check('login', 5, MINUTE) === false) {
return redirect()->back()
->with('erro', 'Tentativas demais. Aguarde.');
}Honeypot adiciona campo invisível ao formulário — bots preenchem (não veem CSS), humanos não. Se preenchido, request é rejeitado silenciosamente. Zero impacto UX. Throttler limita tentativas por IP/ação (rate limiting). Combinar honeypot + CSRF + throttle para proteção robusta. Alternativa ao CAPTCHA sem fricção para utilizadores.
Segurança de cookies
// Configurar sessão (Config/App.php):
public string $sessionCookieName = 'ci_session';
public int $sessionExpiration = 7200;
public string $sessionSavePath = WRITEPATH . 'session';
public bool $sessionMatchIP = false;
public string $sessionTimeToUpdate = 300;
public bool $sessionRegenerateDestroy = true;
// Cookie seguro (Config/Cookie.php):
public string $prefix = '';
public int $expires = 0;
public string $path = '/';
public string $domain = '';
public bool $secure = true; // só HTTPS
public bool $httponly = true; // sem JS access
public string $samesite = 'Lax'; // anti-CSRF
// Regenerar sessão (após login):
session()->regenerate(true);
// Definir cookie manual:
$this->response->setCookie('preferencia', $valor, [
'httponly' => true,
'secure' => true,
]);Cookies de sessão devem ser secure (só HTTPS), httponly (inacessível via JavaScript) e samesite = Lax (anti-CSRF). sessionRegenerateDestroy destrói sessão antiga ao regenerar. Regenerar sessão após login previne session fixation. sessionMatchIP adiciona segurança mas pode quebrar em redes móveis. Expiração de 2h é razoável.
Encriptação e hashing
// Encriptação simétrica:
$encrypter = \Config\Services::encrypter();
$cifrada = $encrypter->encrypt('dados secretos');
$original = $encrypter->decrypt($cifrada);
// Configurar chave (Config/Encryption.php):
public string $key = 'sua-chave-32-bytes-aqui!';
public string $driver = 'OpenSSL';
// Hash de passwords (NUNCA encrypt):
$hash = password_hash($password, PASSWORD_DEFAULT);
// Verificar password:
if (password_verify($input, $hash)) {
// password correta
}
// Verificar se precisa rehash:
if (password_needs_rehash($hash, PASSWORD_DEFAULT)) {
$novoHash = password_hash($password, PASSWORD_DEFAULT);
}
// HMAC:
$assinatura = hash_hmac('sha256', $dados, $chave);Encriptação (encrypter) é reversível — para dados que precisa ler depois. Hash (password_hash) é irreversível — para passwords. Nunca encriptar passwords. password_verify() compara hash sem expor a password. password_needs_rehash() atualiza hash quando o algoritmo muda. Chave de encriptação deve estar no .env, nunca no código.
Content Security Policy
// Ativar (Config/App.php):
public bool $CSPEnabled = true;
// Configurar (Config/ContentSecurityPolicy.php):
public $defaultSrc = 'self';
public $scriptSrc = ['self', 'cdn.jsdelivr.net'];
public $styleSrc = ['self', 'fonts.googleapis.com'];
public $imgSrc = ['self', 'data:', 'https:'];
public $fontSrc = ['fonts.gstatic.com'];
public $connectSrc = 'self';
public $objectSrc = 'none';
public $frameAncestors = 'none';
// Headers de segurança adicionais:
// Config/Filters.php -> 'secureheaders'
public array $globals = [
'after' => ['secureheaders'],
];
// Headers adicionados:
// X-Content-Type-Options: nosniff
// X-Frame-Options: SAMEORIGIN
// X-XSS-Protection: 1; mode=blockCSP controla de onde o browser pode carregar recursos — previne XSS e injeção de scripts. defaultSrc = 'self' permite só recursos do próprio domínio. Whitelists para CDNs e fonts. secureheaders adiciona headers de segurança (nosniff, frame options). frameAncestors = 'none' previne clickjacking. Essencial para aplicações com dados sensíveis.
Filtros (Middleware)
// app/Filters/AuthFilter.php
namespace App\Filters;
use CodeIgniter\Filters\FilterInterface;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
class AuthFilter implements FilterInterface
{
public function before(RequestInterface $request, $arguments = null)
{
if (!session()->get('logged_in')) {
return redirect()->to('/login')
->with('erro', 'Faça login primeiro');
}
}
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
{
// Pós-processamento (opcional)
}
}
// Registar (Config/Filters.php):
public array $aliases = [
'auth' => \App\Filters\AuthFilter::class,
];
// Aplicar:
$routes->get('admin', 'Admin::index', ['filter' => 'auth']);Filtros implementam FilterInterface com before() e after(). before() pode retornar redirect/response para abortar. Registar em Config/Filters.php com alias. Aplicar por rota, grupo ou globalmente. Ideais para autenticação, autorização, logging, rate limiting. $arguments recebe parâmetros do filtro (filter:auth[admin]).
Validação de permissões
// Filtro de roles:
class RoleFilter implements FilterInterface
{
public function before(RequestInterface $request, $arguments = null)
{
$role = session('role');
if (!in_array($role, $arguments)) {
return redirect()->to('/dashboard')
->with('erro', 'Sem permissão');
}
}
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) {}
}
// Registar:
'role' => \App\Filters\RoleFilter::class,
// Aplicar com parâmetros:
$routes->group('admin', ['filter' => 'role:admin,superadmin'],
function ($routes) {
$routes->get('users', 'Admin\Users::index');
$routes->delete('users/(:num)', 'Admin\Users::delete/$1');
}
);Filtro de roles verifica permissões via argumentos (role:admin,superadmin). $arguments recebe a lista de roles permitidos. Aplicar por grupo protege todas as rotas internas. Combinar com filtro auth (login primeiro, role depois). Para RBAC completo, considerar codeigniter4/shield com grupos e permissões. Nunca confiar só em esconder links — validar no servidor.
Recursos Avançados
Cache
$cache = \Config\Services::cache();
// Guardar (chave, dados, segundos):
$cache->save('produtos_populares', $dados, 3600);
// Ler:
$valor = $cache->get('produtos_populares');
if ($valor === null) {
$valor = $model->getPopulares();
$cache->save('produtos_populares', $valor, 3600);
}
// Apagar:
$cache->delete('produtos_populares');
// Limpar tudo:
$cache->clean();
// Cache de página (no controller):
public function index()
{
$this->cachePage(300); // 5 minutos
// ...
}
// Configurar driver (Config/Cache.php):
public string $handler = 'file';
// Opções: file, memcached, redis, predis, wincacheCache reduz queries repetidas e tempo de resposta. save() armazena com TTL (segundos). get() retorna null se expirado. cachePage() cacheia a resposta inteira. Drivers: file (default, sem config), redis/memcached (produção, partilhado). Invalidar cache ao atualizar dados. Padrão: verificar cache → se miss, query + save.
$email = \Config\Services::email();
// Configurar (Config/Email.php):
// SMTP:
public string $protocol = 'smtp';
public string $SMTPHost = 'smtp.gmail.com';
public string $SMTPUser = 'user@gmail.com';
public string $SMTPPass = 'app-password';
public int $SMTPPort = 587;
public string $SMTPCrypto = 'tls';
// Enviar:
$email->setFrom('noreply@site.com', 'Minha App');
$email->setTo('user@mail.com');
$email->setCC('admin@site.com');
$email->setSubject('Confirmação de Registo');
$email->setMessage('<h1>Bem-vindo!</h1>');
// HTML:
$email->setMailType('html');
// Anexo:
$email->attach('/path/to/ficheiro.pdf');
if ($email->send()) {
// enviado
} else {
log_message('error', $email->printDebugger());
}Serviço de email suporta SMTP, sendmail e mail(). Configuração em Config/Email.php ou .env. setMailType('html') para emails HTML. attach() adiciona anexos. printDebugger() mostra erros de envio. Para Gmail, usar App Password (não a password da conta). Em produção, considerar serviços como Mailgun/SendGrid via SMTP.
Time e datas
use CodeIgniter\I18n\Time;
// Agora:
$agora = Time::now();
$agora = Time::now('Europe/Lisbon', 'pt_PT');
// Criar:
$data = Time::create(2024, 12, 25, 10, 30);
$data = Time::parse('2024-12-25 10:30:00');
// Formatar:
echo $agora->format('d/m/Y H:i'); // 25/12/2024 10:30
echo $agora->toDateString(); // 2024-12-25
echo $agora->humanize(); // "há 2 horas"
// Manipular:
$amanha = $agora->addDays(1);
$ontem = $agora->subMonths(2);
$inicio = $agora->startOfMonth();
// Comparar:
if ($agora->isAfter($prazo)) { }
$diferenca = $agora->difference($outra);
echo $diferenca->getDays(); // dias
// Timezone:
$lisboa = $agora->setTimezone('Europe/Lisbon');Time é a classe de datas do CI4 (wraps DateTime/Carbon-like). Time::now() com timezone e locale. format() formata, humanize() retorna texto legível ("há 2 horas"). Métodos fluent: addDays(), subMonths(), startOfMonth(). difference() calcula intervalo. Sempre usar timezone explícito. Configurar default em Config/App.php.
Eventos e hooks
// app/Config/Events.php
use CodeIgniter\Events\Events;
// Ouvir evento:
Events::on('DBQuery', function ($query) {
log_message('debug', (string) $query);
});
Events::on('pre_system', function () {
// Antes de qualquer controller
});
// Disparar evento custom:
Events::trigger('pedido_criado', $pedidoId, $total);
// Ouvir custom:
Events::on('pedido_criado', function ($id, $total) {
// Enviar email de confirmação
// Atualizar stock
// Notificar admin
});
// Prioridade (menor = primeiro):
Events::on('pedido_criado', $fn1, 10);
Events::on('pedido_criado', $fn2, 50);
// Remover listener:
Events::removeListener('pedido_criado', $fn1);Eventos permitem desacoplar lógica — disparar sem saber quem ouve. Events::on() regista listener, Events::trigger() dispara. Prioridade controla ordem de execução. Eventos built-in: pre_system, post_system, DBQuery. Ideais para notificações, logging, analytics sem acoplar ao controller. Similar a observers/pub-sub.
Serviços (DI)
// Serviços built-in:
$request = service('request');
$session = service('session');
$cache = service('cache');
$logger = service('logger');
$db = \Config\Database::connect();
// Serviço personalizado (Config/Services.php):
public static function pagamento(bool $getShared = true)
{
if ($getShared) {
return static::getSharedInstance('pagamento');
}
return new \App\Libraries\PagamentoService();
}
// Uso:
$pagamento = service('pagamento');
$pagamento->cobrar(49.99);
// Injeção em controllers:
class Loja extends BaseController
{
protected $pagamento;
public function initController($req, $res, $log)
{
parent::initController($req, $res, $log);
$this->pagamento = service('pagamento');
}
}Serviços são singletons geridos pelo container DI do CI4. service('nome') obtém instância partilhada. Serviços custom em Config/Services.php. $getShared controla singleton vs nova instância. Ideais para bibliotecas, gateways de pagamento, APIs externas. Facilitam testing (mock de serviços). Todos os serviços core são acessíveis via service().
HTTP Client (CURLRequest)
$client = \Config\Services::curlrequest();
// GET:
$response = $client->get('https://api.exemplo.com/dados', [
'headers' => ['Authorization' => 'Bearer ' . $token],
'timeout' => 10,
]);
// POST JSON:
$response = $client->post('https://api.exemplo.com/users', [
'json' => ['nome' => 'Ana', 'email' => 'ana@mail.com'],
'headers' => ['Accept' => 'application/json'],
]);
// Resposta:
$statusCode = $response->getStatusCode();
$body = $response->getBody();
$dados = json_decode($body, true);
// Com autenticação:
$response = $client->request('GET', $url, [
'auth' => ['user', 'pass'],
]);
// Tratamento de erros:
try {
$response = $client->get($url);
} catch (\CodeIgniter\HTTP\Exceptions\HTTPException $e) {
log_message('error', 'API falhou: ' . $e->getMessage());
}CURLRequest é o HTTP client built-in do CI4 (baseado em CURL). Suporta GET, POST, PUT, DELETE com headers, JSON, auth e timeout. getStatusCode() e getBody() para processar resposta. json option serializa e define Content-Type automaticamente. Envolver em try/catch para erros de rede. Ideal para consumir APIs externas.
Migrations
// Criar migration:
php spark make:migration CriarProdutos
// app/Database/Migrations/2024-01-01-000000_CriarProdutos.php
public function up()
{
$this->forge->addField([
'id' => ['type' => 'INT', 'auto_increment' => true],
'nome' => ['type' => 'VARCHAR', 'constraint' => 200],
'preco' => ['type' => 'DECIMAL', 'constraint' => '10,2'],
'ativo' => ['type' => 'TINYINT', 'default' => 1],
'created_at' => ['type' => 'DATETIME', 'null' => true],
'updated_at' => ['type' => 'DATETIME', 'null' => true],
]);
$this->forge->addKey('id', true);
$this->forge->addKey('nome');
$this->forge->createTable('produtos');
}
public function down()
{
$this->forge->dropTable('produtos');
}
// Executar:
php spark migrate
php spark migrate:rollbackMigrations versionam a estrutura da BD — up() cria, down() reverte. forge é o schema builder: addField(), addKey(), createTable(). php spark migrate aplica pendentes, migrate:rollback reverte. Essencial para trabalho em equipa e deploy consistente. Nunca alterar tabelas manualmente em produção.
Logging
// Níveis de log:
log_message('emergency', 'Sistema em baixo');
log_message('alert', 'Ação imediata necessária');
log_message('critical', 'Erro crítico');
log_message('error', 'Erro na operação');
log_message('warning', 'Algo inesperado');
log_message('notice', 'Evento normal significativo');
log_message('info', 'Informação geral');
log_message('debug', 'Dados de depuração');
// Configurar (Config/Logger.php):
public $threshold = 4; // 1-9 (4 = error+)
// 9 = tudo, 1 = só emergency
// Logs em: writable/logs/log-2024-01-15.log
// Log com contexto:
log_message('error', 'Falha no pagamento: {id}', [
'id' => $pedidoId,
]);
// Handler custom (email, Slack, etc):
// Config/Logger.php -> $handlerslog_message() regista eventos com nível de severidade (PSR-3). $threshold controla quais níveis são gravados (9 = tudo, 4 = error+). Logs em writable/logs/ com data no nome. Placeholders ({id}) com contexto. Em produção, threshold 4 (só erros). Em desenvolvimento, 9 (tudo). Handlers custom podem enviar para email, Slack ou serviços externos.
Seeders
// Criar seeder:
php spark make:seeder ProdutosSeeder
// app/Database/Seeds/ProdutosSeeder.php
namespace App\Database\Seeds;
use CodeIgniter\Database\Seeder;
class ProdutosSeeder extends Seeder
{
public function run()
{
$dados = [
['nome' => 'TV', 'preco' => 500],
['nome' => 'Rádio', 'preco' => 80],
['nome' => 'PC', 'preco' => 1200],
];
$this->db->table('produtos')->insertBatch($dados);
// Chamar outro seeder:
$this->call('CategoriasSeeder');
}
}
// Executar:
php spark db:seed ProdutosSeeder
// Com Faker:
$faker = \Faker\Factory::create();
$nome = $faker->name;Seeders populam a BD com dados iniciais ou de teste. insertBatch() insere múltiplas linhas. $this->call() encadeia seeders. php spark db:seed executa. Faker (via devkit) gera dados realistas. Ideais para dados de referência (categorias, países) e desenvolvimento. Separar seeders de produção e desenvolvimento.
Localização (i18n)
// app/Language/pt/App.php
return [
'bemVindo' => 'Bem-vindo, {0}!',
'itens' => '{0, number} itens no carrinho',
'erro' => [
'naoEncontrado' => 'Página não encontrada',
'semPermissao' => 'Acesso negado',
],
];
// app/Language/en/App.php
return [
'bemVindo' => 'Welcome, {0}!',
];
// Uso:
echo lang('App.bemVindo', [$nome]);
echo lang('App.erro.naoEncontrado');
// Configurar locale:
// Config/App.php:
public string $defaultLocale = 'pt';
public bool $negotiateLocale = true;
public array $supportedLocales = ['pt', 'en', 'es'];
// Mudar em runtime:
service('request')->setLocale('en');Traduções ficam em app/Language/{locale}/ como arrays PHP. lang('App.chave') retorna a tradução. Placeholders ({0}) substituem parâmetros. negotiateLocale deteta idioma do browser. supportedLocales limita idiomas disponíveis. Organizar por ficheiro (App, Validation, Errors). Essencial para aplicações multi-idioma.
API RESTful
Controller RESTful
<?php
namespace App\Controllers;
use CodeIgniter\RESTful\ResourceController;
class ApiProdutos extends ResourceController
{
protected $modelName = 'App\Models\ProdutoModel';
protected $format = 'json';
// GET /api/produtos
public function index()
{
return $this->respond($this->model->findAll());
}
// GET /api/produtos/1
public function show($id = null)
{
$produto = $this->model->find($id);
if (!$produto) {
return $this->failNotFound('Produto não encontrado');
}
return $this->respond($produto);
}
// POST /api/produtos
public function create()
{
$dados = $this->request->getJSON(true);
$id = $this->model->insert($dados);
$produto = $this->model->find($id);
return $this->respondCreated($produto);
}
// PUT /api/produtos/1
public function update($id = null)
{
$dados = $this->request->getJSON(true);
$this->model->update($id, $dados);
return $this->respond($this->model->find($id));
}
// DELETE /api/produtos/1
public function delete($id = null)
{
$this->model->delete($id);
return $this->respondDeleted(['id' => $id]);
}
}ResourceController fornece métodos REST padronizados (index, show, create, update, delete). respond() retorna 200, respondCreated() retorna 201, failNotFound() retorna 404. O $format = 'json' define o Content-Type automaticamente. Rotas com $routes->resource('api/produtos') mapeia todos os verbos HTTP.
Paginação em API
public function index()
{
$pagina = $this->request->getGet('page') ?? 1;
$porPagina = $this->request->getGet('per_page') ?? 20;
$produtos = $this->model
->orderBy('created_at', 'DESC')
->paginate($porPagina, 'default', $pagina);
$pager = $this->model->pager;
return $this->respond([
'data' => $produtos,
'meta' => [
'total' => $pager->getTotal(),
'por_pagina' => $porPagina,
'pagina' => $pager->getCurrentPage(),
'total_paginas' => $pager->getPageCount(),
],
]);
}paginate() limita resultados e calcula offsets automaticamente. Parâmetros: itens por página, grupo e número da página. O objecto $pager fornece metadados: getTotal() (total de registos), getCurrentPage(), getPageCount(). Incluir metadados na resposta permite ao cliente construir navegação. Parâmetros via query string (?page=2&per_page=50).
CORS em API
// Config/Filters.php — alias:
public array $aliases = [
'cors' => \App\Filters\CorsFilter::class,
];
// App/Filters/CorsFilter.php:
public function before(RequestInterface $request, $arguments = null)
{
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
// Responder preflight OPTIONS:
if ($request->getMethod() === 'options') {
$response = service('response');
$response->setStatusCode(200);
return $response;
}
}
// Aplicar a grupo:
$routes->group('api', ['filter' => 'cors'], function ($routes) {
$routes->resource('produtos');
});CORS permite que browsers de outros domínios acedam à API. Headers Access-Control-Allow-* definem origens, métodos e headers permitidos. Requests OPTIONS (preflight) devem retornar 200 vazio. Em produção, substituir * por domínios específicos. Aplicar como filtro no grupo api para não afectar rotas web.
Resource Routes
// Config/Routes.php:
// Mapeia CRUD completo:
$routes->resource('api/produtos', [
'controller' => 'ApiProdutos',
]);
// Gera:
// GET /api/produtos → index()
// GET /api/produtos/(:num) → show($1)
// POST /api/produtos → create()
// PUT /api/produtos/(:num) → update($1)
// DELETE /api/produtos/(:num) → delete($1)
// Apenas alguns métodos:
$routes->resource('api/users', [
'only' => ['index', 'show'],
]);
// Excluir métodos:
$routes->resource('api/posts', [
'except' => ['delete'],
]);
// Websafe (form em vez de PUT/DELETE):
$routes->resource('api/itens', [
'websafe' => true,
]);
// PUT → POST com _method=PUT$routes->resource() gera as 5 rotas REST automaticamente. only limita métodos disponíveis, except exclui específicos. websafe converte PUT/DELETE em POST com campo _method (para formulários HTML que só suportam GET/POST). Alternativa: $routes->presenter() inclui new() e edit() para formulários.
Filtros e pesquisa
public function index()
{
$builder = $this->model;
// Pesquisa por texto:
if ($busca = $this->request->getGet('q')) {
$builder = $builder->like('nome', $busca);
}
// Filtro exacto:
if ($status = $this->request->getGet('status')) {
$builder = $builder->where('status', $status);
}
// Filtro de intervalo:
if ($min = $this->request->getGet('preco_min')) {
$builder = $builder->where('preco >=', $min);
}
// Ordenação:
$ordenar = $this->request->getGet('sort') ?? 'created_at';
$direcao = $this->request->getGet('dir') ?? 'DESC';
$builder = $builder->orderBy($ordenar, $direcao);
return $this->respond($builder->paginate(20));
}Filtros são opcionais — aplicados apenas se o parâmetro existir na query string. like() para pesquisa parcial, where() para igualdade exacta. Encadear condições no $builder permite combinações dinâmicas. Validar nomes de coluna para ordenação (whitelist) evita SQL injection. Padrão RESTful: ?q=termo&status=ativo&sort=nome&dir=ASC.
Transformers / Formatação
// Formatar saída da API:
private function formatarProduto(array $produto): array
{
return [
'id' => (int) $produto['id'],
'nome' => $produto['nome'],
'preco' => number_format($produto['preco'], 2, '.', ''),
'categoria' => $produto['categoria_nome'] ?? null,
'links' => [
'self' => base_url('api/produtos/' . $produto['id']),
],
];
}
public function index()
{
$produtos = $this->model->findAll();
$dados = array_map([$this, 'formatarProduto'], $produtos);
return $this->respond($dados);
}
// Com relações:
public function show($id = null)
{
$produto = $this->model
->select('produtos.*, categorias.nome as categoria_nome')
->join('categorias', 'categorias.id = produtos.categoria_id', 'left')
->find($id);
return $this->respond($this->formatarProduto($produto));
}Transformers formatam dados antes de enviar — nunca expor a estrutura interna da base de dados. Converter tipos ((int)), formatar valores (number_format) e incluir links HATEOAS. array_map() aplica a transformação a colecções. Manter campos consistentes entre index e show. Para projectos maiores, criar classes Transformer dedicadas.
Respostas JSON
// Respostas com status codes:
return $this->respond($dados); // 200
return $this->respondCreated($dados); // 201
return $this->respondDeleted($dados); // 200
// Erros:
return $this->fail('Erro genérico'); // 400
return $this->failUnauthorized('Token inválido'); // 401
return $this->failForbidden('Sem permissão'); // 403
return $this->failNotFound('Não encontrado'); // 404
return $this->failValidationError($erros); // 422
return $this->failServerError('Erro interno'); // 500
// Resposta manual:
return $this->response
->setStatusCode(202)
->setJSON(['status' => 'aceite']);
// Formato da resposta:
// { "status": 200, "error": null, "messages": [], "data": {...} }Métodos respond*() e fail*() padronizam a estrutura JSON com status, error, messages e data. Cada método define o HTTP status code correto automaticamente. failValidationError() aceita array de erros de validação. Para respostas customizadas, usar setStatusCode() + setJSON() directamente.
Autenticação com token
// Filtro de autenticação (App/Filters/AuthApi.php):
namespace App\Filters;
use CodeIgniter\Filters\FilterInterface;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
class AuthApi implements FilterInterface
{
public function before(RequestInterface $request, $arguments = null)
{
$token = $request->getHeaderLine('Authorization');
$token = str_replace('Bearer ', '', $token);
if (empty($token)) {
return service('response')
->setStatusCode(401)
->setJSON(['error' => 'Token ausente']);
}
$user = model('UserModel')
->where('api_token', hash('sha256', $token))
->first();
if (!$user) {
return service('response')
->setStatusCode(401)
->setJSON(['error' => 'Token inválido']);
}
// Guardar user para uso posterior:
$request->user = $user;
}
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
{
}
}Filtros interceptam requests antes do controller. O token vem no header Authorization: Bearer xxx. Armazenar hash (não token plain) na base de dados. Retornar resposta 401 directamente no before() bloqueia o acesso. Registar em Config/Filters.php como alias e aplicar a rotas: $routes->group('api', ['filter' => 'authapi']).
Validação em API
public function create()
{
$dados = $this->request->getJSON(true);
$regras = [
'nome' => 'required|min_length[3]|max_length[100]',
'email' => 'required|valid_email|is_unique[users.email]',
'idade' => 'permit_empty|integer|greater_than[0]',
];
if (!$this->validate($regras)) {
return $this->failValidationError(
$this->validator->getErrors()
);
}
$id = $this->model->insert($dados);
return $this->respondCreated(
$this->model->find($id),
'Recurso criado'
);
}Em APIs, $this->request->getJSON(true) converte o body JSON em array associativo. A validação usa as mesmas regras do formulário. failValidationError() retorna 422 com os erros detalhados em JSON. getErrors() retorna array de mensagens por campo. O segundo parâmetro de respondCreated() é a mensagem de sucesso.
Rate limiting
// Filtro simples de rate limit (App/Filters/RateLimit.php):
public function before(RequestInterface $request, $arguments = null)
{
$ip = $request->getIPAddress();
$cache = service('cache');
$key = 'rate_' . $ip;
$hits = $cache->get($key) ?? 0;
$limite = 60; // requests por minuto
if ($hits >= $limite) {
return service('response')
->setStatusCode(429)
->setHeader('Retry-After', '60')
->setJSON(['error' => 'Limite excedido']);
}
$cache->save($key, $hits + 1, 60);
}
// Aplicar globalmente (Config/Filters.php):
public array $globals = [
'before' => ['ratelimit'],
];Rate limiting protege APIs contra abuso. Usa cache para contar requests por IP com TTL de 60 segundos. Ao exceder o limite, retorna 429 Too Many Requests com header Retry-After. Aplicar como filtro global ou por grupo de rotas. Para produção, preferir Redis (contador partilhado entre servidores). Headers informativos: X-RateLimit-Limit, X-RateLimit-Remaining.
CLI e Ferramentas
Comando Spark
// spark é o CLI do CodeIgniter 4: php spark // Comandos built-in: php spark serve // Servidor de desenvolvimento php spark routes // Listar todas as rotas php spark db:seed // Executar seeders php spark migrate // Executar migrações php spark migrate:rollback // Reverter migrações php spark make:controller Nome // Gerar controller php spark make:model Nome // Gerar model php spark make:migration Nome // Gerar migração php spark make:command Nome // Gerar comando php spark make:filter Nome // Gerar filtro php spark make:seeder Nome // Gerar seeder php spark cache:clear // Limpar cache php spark db:table // Listar tabelas
php spark é a interface CLI do CI4 (equivalente ao artisan do Laravel). serve inicia servidor na porta 8080. routes mostra todas as rotas registadas com verbos e handlers. make:* gera boilerplate com namespace correcto. migrate e db:seed gerem a base de dados. Todos os comandos aceitam --help para documentação.
Seeders e dados
// Criar: php spark make:seeder ProdutosSeeder
// App/Database/Seeds/ProdutosSeeder.php:
namespace App\Database\Seeds;
use CodeIgniter\Database\Seeder;
class ProdutosSeeder extends Seeder
{
public function run()
{
$dados = [
[
'nome' => 'Produto A',
'preco' => 29.90,
'activo' => 1,
'created_at' => date('Y-m-d H:i:s'),
],
[
'nome' => 'Produto B',
'preco' => 49.90,
'activo' => 1,
'created_at' => date('Y-m-d H:i:s'),
],
];
$this->db->table('produtos')->insertBatch($dados);
// Chamar outro seeder:
$this->call('CategoriasSeeder');
}
}
// Executar:
// php spark db:seed ProdutosSeederSeeders populam a base de dados com dados iniciais. insertBatch() insere múltiplas linhas eficientemente. $this->call() encadeia seeders (ordem de execução). $this->db dá acesso à conexão. Para dados de teste, usar Faker (integrado no CI4). Executar com php spark db:seed NomeSeeder. Registar seeders no DatabaseSeeder para execução em conjunto.
Configuração de ambientes
// .env (raiz do projecto): # CI_ENVIRONMENT = production | development | testing CI_ENVIRONMENT = development # Base URL: app.baseURL = 'http://localhost:8080/' # Database: database.default.hostname = localhost database.default.database = minha_app database.default.username = root database.default.password = '' database.default.DBDriver = MySQLi # Email: email.protocol = smtp email.SMTPHost = smtp.gmail.com email.SMTPUser = user@gmail.com email.SMTPPass = senha_app email.SMTPPort = 587 email.SMTPCrypto = tls # Em produção: # CI_ENVIRONMENT = production # (desactiva debugbar, mostra página de erro genérica)
O ficheiro .env configura o ambiente sem alterar código. CI_ENVIRONMENT controla debug: development mostra erros detalhados + debugbar, production esconde detalhes. Configs usam notação grupo.propriedade. Nunca commitar .env — usar .env.example como template. Em produção, definir baseURL, database e email com valores reais.
Criar comando custom
<?php
namespace App\Commands;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
class GerarRelatorio extends BaseCommand
{
protected $group = 'App';
protected $name = 'relatorio:gerar';
protected $description = 'Gera relatório mensal de vendas';
protected $usage = 'relatorio:gerar [mes] [ano]';
protected $arguments = [
'mes' => 'Mês (1-12)',
'ano' => 'Ano (4 dígitos)',
];
protected $options = [
'--formato' => 'Formato: csv, pdf, excel',
];
public function run(array $params)
{
$mes = $params[0] ?? date('m');
$ano = $params[1] ?? date('Y');
$formato = CLI::getOption('formato') ?? 'csv';
CLI::write("Gerando relatório: {$mes}/{$ano}");
CLI::write("Formato: {$formato}", 'green');
// Lógica do relatório...
$total = model('VendaModel')
->where('MONTH(created_at)', $mes)
->where('YEAR(created_at)', $ano)
->selectSum('total')
->first();
CLI::write("Total: R$ {$total['total']}", 'yellow');
CLI::newLine();
}
}Comandos custom estendem BaseCommand em App/Commands/. $name define como invocar (php spark relatorio:gerar 06 2024). $arguments são posicionais, $options usam --flag. CLI::write() imprime com cor (green, yellow, red). CLI::getOption() lê flags. $group organiza no output de php spark.
Faker / Dados de teste
// Em Seeders (Faker integrado):
use Faker\Factory;
public function run()
{
$faker = Factory::create('pt_BR');
$dados = [];
for ($i = 0; $i < 50; $i++) {
$dados[] = [
'nome' => $faker->name(),
'email' => $faker->unique()->safeEmail(),
'telefone' => $faker->phoneNumber(),
'morada' => $faker->address(),
'empresa' => $faker->company(),
'created_at' => $faker->dateTimeThisYear()->format('Y-m-d H:i:s'),
];
}
$this->db->table('clientes')->insertBatch($dados);
}
// Factories (CI4 built-in):
use CodeIgniter\Test\Fabricator;
use App\Models\UserModel;
$users = fabricate(UserModel::class, 10, [
'activo' => 1,
]);Faker gera dados realistas para testes e desenvolvimento. Locale pt_BR para dados em português. unique() evita duplicados. insertBatch() para performance com muitos registos. fabricate() é o helper do CI4 que combina Factory + Faker + Model. Nunca usar seeders com Faker em produção — apenas para desenvolvimento e testes.
Deploy e optimização
// Checklist de produção:
// 1. Ambiente:
CI_ENVIRONMENT = production
// 2. Cache de configurações:
php spark cache:clear
// 3. Optimizar autoloader (composer):
composer install --no-dev --optimize-autoloader
// 4. Estrutura no servidor:
// /public_html/ ← apontar document root aqui
// index.php
// assets/
// /app/ ← fora do document root
// /vendor/
// /writable/ ← permissões 755
// 5. index.php (apontar para ../):
// $pathsConfig = FCPATH . '../app/Config/Paths.php';
// 6. .htaccess (Apache):
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php/$1 [L]
// 7. Segurança:
// - Desactivar listagem de directórios
// - Proteger /writable e /app
// - HTTPS obrigatórioEm produção, o document root deve apontar para /public/ — o resto fica inacessível via web. CI_ENVIRONMENT = production desactiva debugging. composer install --no-dev exclui dependências de desenvolvimento. .htaccess reescreve URLs para o front controller. Proteger /writable e /app com regras de acesso. Usar HTTPS e headers de segurança.
CLI input/output
use CodeIgniter\CLI\CLI;
// Output com cores:
CLI::write('Sucesso!', 'green');
CLI::write('Atenção!', 'yellow');
CLI::write('Erro!', 'red');
CLI::error('Falha crítica'); // vermelho + stderr
// Input do utilizador:
$nome = CLI::prompt('Qual o seu nome?');
$email = CLI::prompt('Email:', null, 'required|valid_email');
// Escolha entre opções:
$tipo = CLI::prompt('Tipo:', ['admin', 'user', 'guest']);
// Mostra: [0] admin [1] user [2] guest
// Confirmação:
if (CLI::prompt('Continuar?', ['y', 'n']) === 'y') {
// prosseguir
}
// Formatação:
CLI::newLine();
CLI::write(str_repeat('-', 40));
CLI::table($dados, ['ID', 'Nome', 'Email']);
// Progresso:
$progress = new \CodeIgniter\CLI\Progressbar(100);
for ($i = 0; $i <= 100; $i++) {
$progress->update($i);
}A classe CLI fornece I/O interactivo para comandos. prompt() aceita validação inline (regras do Validation). CLI::table() formata arrays em tabela ASCII. Cores: green, yellow, red, blue, magenta, cyan. CLI::error() escreve em stderr. Progressbar mostra progresso em loops longos. Input com validação repete a pergunta até valor válido.
Testing
// tests/unit/CalculadoraTest.php:
namespace Tests\Unit;
use CodeIgniter\Test\CIUnitTestCase;
class CalculadoraTest extends CIUnitTestCase
{
public function testSomar()
{
$resultado = 2 + 3;
$this->assertEquals(5, $resultado);
}
public function testDivisaoPorZero()
{
$this->expectException(\DivisionByZeroError::class);
$resultado = 1 / 0;
}
}
// Test de feature (HTTP):
namespace Tests\Feature;
use CodeIgniter\Test\FeatureTestTrait;
use CodeIgniter\Test\CIUnitTestCase;
class ApiTest extends CIUnitTestCase
{
use FeatureTestTrait;
public function testListarProdutos()
{
$result = $this->get('api/produtos');
$result->assertOK();
$result->assertJSONFragment(['nome' => 'Produto A']);
}
}
// Executar: php spark test
// Ou: vendor/bin/phpunitCI4 usa PHPUnit integrado. CIUnitTestCase é a classe base com helpers do framework. FeatureTestTrait permite testar rotas HTTP sem servidor (get(), post()). Assertions: assertOK() (200), assertJSONFragment(), assertStatus(). expectException() para erros. Configurar em phpunit.xml. Executar com php spark test.
Estrutura do projecto
// Estrutura CI4: project/ ├── app/ │ ├── Config/ // Configurações │ │ ├── Routes.php // Rotas │ │ ├── Database.php │ │ ├── Validation.php │ │ └── Filters.php │ ├── Controllers/ // Controllers │ ├── Models/ // Models │ ├── Views/ // Templates │ ├── Database/ │ │ ├── Migrations/ // Migrações │ │ └── Seeds/ // Seeders │ ├── Filters/ // Filtros (middleware) │ ├── Libraries/ // Classes auxiliares │ └── Helpers/ // Funções helper ├── public/ // Document root │ ├── index.php // Front controller │ └── assets/ // CSS, JS, imagens ├── writable/ // Logs, cache, uploads ├── tests/ // Testes ├── vendor/ // Dependências Composer ├── spark // CLI └── .env // Config de ambiente
A estrutura segue MVC com separação clara. app/ contém toda a lógica. public/ é o único directório acessível via web (front controller index.php). writable/ armazena logs, cache e uploads (precisa de permissão de escrita). Filters/ são middleware. spark é o CLI. Configs em app/Config/ — uma classe por aspecto do sistema.
Migrações
// Criar: php spark make:migration criar_produtos
// App/Database/Migrations/2024-01-15-120000_criar_produtos.php:
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CriarProdutos extends Migration
{
public function up()
{
$this->forge->addField([
'id' => ['type' => 'INT', 'auto_increment' => true],
'nome' => ['type' => 'VARCHAR', 'constraint' => 200],
'preco' => ['type' => 'DECIMAL', 'constraint' => '10,2'],
'activo' => ['type' => 'TINYINT', 'default' => 1],
'created_at' => ['type' => 'DATETIME', 'null' => true],
'updated_at' => ['type' => 'DATETIME', 'null' => true],
]);
$this->forge->addKey('id', true);
$this->forge->addKey('activo');
$this->forge->createTable('produtos');
}
public function down()
{
$this->forge->dropTable('produtos', true);
}
}
// Executar:
// php spark migrate
// php spark migrate:rollback
// php spark migrate:refresh (rollback + migrate)Migrações versionam o schema da base de dados. $this->forge é o builder de DDL. addField() define colunas com tipo, constraint e default. addKey() cria índices (segundo parâmetro true = primary key). up() aplica, down() reverte. Ficheiros ordenados por timestamp. migrate:refresh recria tudo do zero.
Debugging e profiling
// Debugbar (activar em .env):
// CI_ENVIRONMENT = development
// Toolbar de debug mostra:
// - Queries SQL com tempo
// - Rotas correspondidas
// - Views renderizadas
// - Headers da request
// - Logs e timeline
// Logging manual:
log_message('error', 'Falha ao processar pedido #' . $id);
log_message('info', 'User login: ' . $email);
log_message('debug', 'Dados: ' . print_r($dados, true));
// dd() e d():
dd($variavel); // dump + die
d($variavel); // dump (continua)
// Benchmark:
$benchmark = service('timer');
$benchmark->start('query');
$dados = $model->findAll();
$benchmark->stop('query');
echo $benchmark->getElapsedTime('query');
// Ver última query SQL:
echo $model->getLastQuery();Em modo development, a Debugbar mostra queries, tempo, rotas e views automaticamente. log_message() grava em writable/logs/ com níveis (error, info, debug). dd() faz dump e para execução. service('timer') mede performance de trechos de código. getLastQuery() mostra o SQL gerado pelo Query Builder para debugging.