Trabalhando com banco de dados
Neste capítulo, vamos criar uma tabela de países, preencher com alguns registros e mostrar tudo numa página com paginação. Para não precisar instalar nada, o banco vai ser SQLite, que guarda tudo num arquivo.
Instalando os pacotes
composer require yiisoft/db-sqlite yiisoft/db-migration yiisoft/cache yiisoft/data yiisoft/data-dbyiisoft/db-sqlite: a conexão com SQLite e o query builderyiisoft/db-migration: os comandos./yii migrate:*yiisoft/cache: a conexão guarda a estrutura das tabelas em cacheyiisoft/dataeyiisoft/data-db: paginação em cima de uma consulta
Confira se o PHP tem a extensão pdo_sqlite com php -m.
Configurando a conexão
No Yii3, configurar um serviço é dizer ao container como montá-lo. Crie config/common/di/db.php:
<?php
declare(strict_types=1);
use Yiisoft\Aliases\Aliases;
use Yiisoft\Cache\ArrayCache;
use Yiisoft\Db\Cache\SchemaCache;
use Yiisoft\Db\Connection\ConnectionInterface;
use Yiisoft\Db\Sqlite\Connection;
use Yiisoft\Db\Sqlite\Driver;
return [
SchemaCache::class => [
'__construct()' => [new ArrayCache()],
],
ConnectionInterface::class => static fn (Aliases $aliases, SchemaCache $schemaCache) => new Connection(
new Driver('sqlite:' . $aliases->get('@runtime/database.sqlite')),
$schemaCache,
),
];Todo arquivo dentro de config/common/di/ é carregado automaticamente, então não precisa registrar este em lugar nenhum. A partir de agora, qualquer classe que pedir ConnectionInterface no construtor recebe esta conexão. O banco fica em runtime/database.sqlite.
Para usar MySQL ou PostgreSQL, você troca o pacote (yiisoft/db-mysql, yiisoft/db-pgsql) e este arquivo. O resto do código continua igual, porque ele depende da interface, e não do banco.
Configurando as migrations
As migrations precisam saber em qual namespace ficam. Em config/console/params.php, adicione a chave yiisoft/db-migration:
<?php
declare(strict_types=1);
return [
'yiisoft/yii-console' => [
'commands' => require __DIR__ . '/commands.php',
],
'yiisoft/db-migration' => [
'newMigrationNamespace' => 'App\Migration',
'sourceNamespaces' => ['App\Migration'],
],
];Criando a tabela
Crie a pasta e peça para o gerador escrever a migration:
mkdir -p src/Migration
./yii migrate:create pais --command=table --fields="codigo:char(2):notNull:unique,nome:string(52):notNull,populacao:integer:notNull"Ele cria um arquivo como src/Migration/M261008043647CreatePaisTable.php, com o número correspondente à data e hora. Abra e adicione, logo depois do createTable(), os registros de exemplo:
final class M261008043647CreatePaisTable implements RevertibleMigrationInterface, TransactionalMigrationInterface
{
public function up(MigrationBuilder $b): void
{
$columnBuilder = $b->columnBuilder();
$b->createTable('pais', [
'id' => $columnBuilder::primaryKey(),
'codigo' => $columnBuilder::char(2)->notNull()->unique(),
'nome' => $columnBuilder::string(52)->notNull(),
'populacao' => $columnBuilder::integer()->notNull(),
]);
$b->insertBatch('pais', [
['AU', 'Austrália', 26_000_000],
['BR', 'Brasil', 212_000_000],
['CA', 'Canadá', 40_000_000],
['CN', 'China', 1_410_000_000],
['DE', 'Alemanha', 83_000_000],
['FR', 'França', 68_000_000],
['GB', 'Reino Unido', 68_000_000],
['IN', 'Índia', 1_440_000_000],
['PT', 'Portugal', 10_600_000],
['US', 'Estados Unidos', 335_000_000],
], ['codigo', 'nome', 'populacao']);
}
public function down(MigrationBuilder $b): void
{
$b->dropTable('pais');
}
}O up() aplica a mudança e o down() desfaz. Como a classe implementa TransactionalMigrationInterface, tudo roda numa transação: se o insert falhar, a tabela também não é criada.
Aplique:
./yii migrate:up > create table pais ... Done in 0.001s.
[OK] Applied (time: 0.081s)Para desfazer a última migration, use ./yii migrate:down.
A página
Crie src/Web/Pais/Action.php:
<?php
declare(strict_types=1);
namespace App\Web\Pais;
use App\Web\NotFound\NotFoundHandler;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Yiisoft\Data\Db\QueryDataReader;
use Yiisoft\Data\Paginator\OffsetPaginator;
use Yiisoft\Db\Connection\ConnectionInterface;
use Yiisoft\Input\Http\Attribute\Parameter\Query;
use Yiisoft\Yii\View\Renderer\WebViewRenderer;
final readonly class Action
{
public function __construct(
private WebViewRenderer $viewRenderer,
private ConnectionInterface $db,
private NotFoundHandler $notFoundHandler,
) {}
public function __invoke(ServerRequestInterface $request, #[Query('pagina')] int $pagina = 1): ResponseInterface
{
$query = $this->db->select()->from('pais')->orderBy(['nome' => SORT_ASC]);
$paginator = (new OffsetPaginator(new QueryDataReader($query)))->withPageSize(5);
if ($pagina < 1 || $pagina > $paginator->getTotalPages()) {
return $this->notFoundHandler->handle($request);
}
return $this->viewRenderer->render(__DIR__ . '/template', [
'paginator' => $paginator->withCurrentPage($pagina),
]);
}
}Vamos por partes:
$this->db->select()->from('pais')->orderBy(...)monta a consulta com o query builder. Ela ainda não foi executada.- O
QueryDataReaderembrulha a consulta, e oOffsetPaginatorcuida doLIMIT, doOFFSETe da contagem. ComwithPageSize(5), são 5 países por página. - Uma página que não existe (
?pagina=99) ou um valor que não é número devolve 404, com a página de erro do template. - O
withCurrentPage()devolve um novo paginador, sem alterar o original. Esse padrãowith*()aparece em todo o Yii3.
Agora a view, em src/Web/Pais/template.php:
<?php
declare(strict_types=1);
use Yiisoft\Html\Html;
/**
* @var Yiisoft\View\WebView $this
* @var Yiisoft\Router\UrlGeneratorInterface $urlGenerator
* @var Yiisoft\Data\Paginator\OffsetPaginator $paginator
*/
$this->setTitle('Países');
?>
<h1>Países</h1>
<ul>
<?php foreach ($paginator->read() as $pais): ?>
<li>
<?= Html::encode($pais['nome']) ?> (<?= Html::encode($pais['codigo']) ?>):
<?= number_format((int) $pais['populacao'], 0, ',', '.') ?>
</li>
<?php endforeach ?>
</ul>
<nav>
<?php for ($pagina = 1; $pagina <= $paginator->getTotalPages(); $pagina++): ?>
<?php if ($pagina === $paginator->getCurrentPage()): ?>
<strong><?= $pagina ?></strong>
<?php else: ?>
<a href="<?= $urlGenerator->generate('pais', queryParameters: ['pagina' => $pagina]) ?>"><?= $pagina ?></a>
<?php endif ?>
<?php endfor ?>
</nav>O read() executa a consulta e devolve só os registros da página atual. Cada registro é um array com as colunas da tabela.
A rota
Route::get('/paises')
->action(Web\Pais\Action::class)
->name('pais'),Testando
Abra http://127.0.0.1:8080/paises. Aparecem os 5 primeiros países em ordem alfabética, de Alemanha a China, e os links para as páginas 1 e 2. A página 2 (/paises?pagina=2) mostra os outros 5, e /paises?pagina=99 dá 404.
Comparando com o Yii2
No Yii2, este exemplo usava um ActiveRecord chamado Country e o Pagination. Aqui, usamos o query builder direto, sem classe de modelo, e o paginador do yiisoft/data. Se você prefere trabalhar com Active Record, ele existe no Yii3 como o pacote opcional yiisoft/active-record, e o capítulo Active Record vai cobrir isso.