Se você vem do Yii2, a primeira coisa que vai procurar no Yii3 é o Yii::$app. Ele não existe. Não tem objeto global da aplicação, não tem Controller base cheio de propriedades mágicas e não tem array de components.
Quando isso encaixa, o resto faz muito mais sentido. Este post é o modelo mental que eu queria ter antes de escrever qualquer código. Se preferir ir direto para a prática, o CRUD passo a passo usa tudo que está aqui.
É um conjunto de pacotes, não um framework monolítico
O Yii3 não é um repositório só. É uma coleção de pacotes yiisoft/* pequenos, e cada um faz uma coisa: yiisoft/router, yiisoft/di, yiisoft/db, yiisoft/validator, yiisoft/view e por aí vai.
Quem junta tudo num app funcionando é o template yiisoft/app. No meu projeto de CRUD, depois de adicionar os pacotes de banco e de formulário, são 49 pacotes yiisoft/* no vendor/. Você não precisa conhecer a maioria, mas ajuda saber que eles existem e que cada um funciona fora do Yii também.
Isso também quer dizer que você adiciona funcionalidades com composer require, e não ativando algo num arquivo de config gigante. Precisa de banco? composer require yiisoft/db-sqlite. Formulários? yiisoft/form-model. E a maioria dos pacotes traz a própria configuração, que é o próximo ponto.
A configuração é array PHP, mesclado de vários lugares
Toda a configuração fica em config/, dividida em grupos. O mapa dos grupos está em config/configuration.php:
'config-plugin' => [
'params' => 'common/params.php',
'params-web' => ['$params', 'web/params.php'],
'params-console' => ['$params', 'console/params.php'],
'di' => 'common/di/*.php',
'di-web' => ['$di', 'web/di/*.php'],
'di-console' => '$di',
'routes' => 'common/routes.php',
'bootstrap' => 'common/bootstrap.php',
// ...
],
O grupo di é todo arquivo em config/common/di/, o di-web é isso mais os arquivos de config/web/di/, e assim por diante. O app web e o console leem grupos diferentes, e os ambientes (dev, prod, test) podem sobrescrever os params.
O detalhe importante é que os pacotes também trazem configuração. O yiisoft/session tem um config/di-web.php que liga FlashInterface a Flash. O yiisoft/db-migration registra os comandos migrate:* no config/params.php dele. Quando você instala um pacote, o yiisoft/config adiciona os arquivos dele ao "merge plan" (config/.merge-plan.php), e em tempo de execução eles são mesclados com os seus.
Então, depois de composer require yiisoft/db-migration, o ./yii migrate:up simplesmente existe, sem nenhuma linha escrita por você. Se você mexer no próprio configuration.php, rode composer yii-config-rebuild para atualizar o plano.
O container constrói tudo
Essa é a maior mudança em relação ao Yii2. No Yii3 você nunca busca um serviço em algum lugar: você pede no construtor, e o container de DI (yiisoft/di, PSR-11) entrega.
final readonly class PostRepository
{
public function __construct(
private ConnectionInterface $db,
) {}
}
Para uma classe concreta como PostRepository, você não escreve configuração nenhuma. O container lê o construtor, monta o que precisa e injeta. Isso é autowiring.
Você só precisa de uma definição quando o container não tem como adivinhar. Isso acontece com uma interface (qual implementação?) ou com um valor escalar (qual DSN?). Esta é a conexão de banco do CRUD, em config/common/di/db.php:
return [
ConnectionInterface::class => static fn (Aliases $aliases, SchemaCache $schemaCache) => new Connection(
new Driver('sqlite:' . $aliases->get('@runtime/database.sqlite')),
$schemaCache,
),
];
Todas as classes do app dependem de ConnectionInterface, nunca de SQLite. Trocar para MySQL é mudar só esse arquivo.
Na prática, faço três perguntas quando escrevo uma classe:
- Do que ela precisa? Isso vai no construtor.
- É uma interface ou um valor? Então precisa de uma definição em
config/*/di/. - Ela precisa saber de HTTP? Se não, fica fora de
src/Web.
Uma requisição, do começo ao fim
Isto é o que acontece quando uma requisição chega no app:
O public/index.php cria um HttpApplicationRunner. O runner monta a configuração e o container, e entrega a requisição para a aplicação. A aplicação roda uma pilha de middlewares PSR-15 definida em config/web/di/application.php:
'withMiddlewares()' => [
[
ErrorCatcher::class,
SessionMiddleware::class,
CsrfTokenMiddleware::class,
RequestCatcherMiddleware::class,
Router::class,
],
],
Cada middleware recebe a requisição, pode fazer algo antes, chama o próximo e pode mexer na resposta depois. O ErrorCatcher é a camada mais externa, então qualquer exceção lançada lá dentro termina nele. O Router é o último: ele encontra a rota e chama a sua action. O que a action retorna volta pelas mesmas camadas, e o SapiEmitter envia para o navegador.
Middleware é como você se encaixa na requisição
Como tudo é middleware, adicionar comportamento em volta de uma requisição segue sempre a mesma ideia. Este aqui adiciona um header com o tempo de resposta:
final class ResponseTimeMiddleware implements MiddlewareInterface
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$start = microtime(true);
$response = $handler->handle($request);
$ms = round((microtime(true) - $start) * 1000, 1);
return $response->withHeader('X-Response-Time', $ms . 'ms');
}
}
Você pode colocar na lista global ou ligar só em algumas rotas:
Group::create('/posts')
->middleware(ResponseTimeMiddleware::class)
->routes(/* ... */),
Autenticação, controle de acesso, CORS e rate limit funcionam todos assim. No meu blog, a área admin é um grupo de rotas com um middleware de permissão, e o grupo da API tem um middleware de token.
Actions são só callables
O template não tem controller base. A home é uma classe só, com __invoke():
final readonly class Action
{
public function __construct(
private WebViewRenderer $viewRenderer,
) {}
public function __invoke(): ResponseInterface
{
return $this->viewRenderer->render(__DIR__ . '/template');
}
}
A rota aponta para ela com ->action(Action::class). Se você prefere controllers com vários métodos, ->action([PostController::class, 'index']) também funciona, e foi o que usei no CRUD. Nos dois casos, a action recebe os serviços pelo construtor e retorna uma resposta PSR-7. Não precisa de classe base.
As coisas são imutáveis
Você vai ver muito método with*() no Yii3: $response->withHeader(), $viewRenderer->withViewPath(), $viewRenderer->withLayout(). Eles não alteram o objeto, retornam um novo.
// o $viewRenderer é compartilhado pelo app todo, então guardamos uma cópia nossa
$this->viewRenderer = $viewRenderer->withViewPath(__DIR__ . '/views');
O motivo é que os serviços são compartilhados. Se um controller mudasse o caminho das views no renderer compartilhado, todos os outros controllers seriam afetados. Com imutabilidade, isso não acontece. É também o que permite rodar o Yii3 em servidores de longa duração como o RoadRunner (existe o yiisoft/yii-runner-roadrunner), onde os mesmos objetos atendem muitas requisições.
Como eu organizo uma funcionalidade
O template já sugere um formato: src/Web/HomePage/Action.php com o template.php ao lado. Eu sigo essa ideia, agrupando por funcionalidade, e separo o que é web do que não é:
src/
Post/
PostRepository.php # fala com o banco, não sabe nada de HTTP
Web/
Post/
PostController.php # HTTP: lê a requisição, devolve a resposta
PostForm.php # o que o formulário aceita + regras de validação
views/
index.php
_form.php
Migration/
M261008033620CreatePostTable.php
Quando adiciono uma funcionalidade, costumo seguir esta ordem:
- Uma migration para a tabela.
- Um repository (ou service) em
src/<Funcionalidade>, sem nada de HTTP. - Um form model, se tiver entrada do usuário.
- Uma action ou controller em
src/Web/<Funcionalidade>. - As rotas em
config/common/routes.php. - As views ao lado do controller.
- Uma definição em
config/*/di/só se algo novo for interface ou precisar de valores de config.
O repository não sabe que está sendo chamado de uma página web. A mesma classe pode ser usada depois num comando de console ou num endpoint de API, sem mudança. No meu blog, o mesmo PostRepository atende tanto as páginas do site quanto a API REST.
Vindo do Yii2
Resumindo: o que era implícito agora é explícito. Yii::$app->db vira um ConnectionInterface no construtor. Os components da config viram definições de DI. behaviors() e filtros viram middleware. Controllers não estendem nada, e o $this->render() vira um WebViewRenderer injetado. Active Record deixou de ser o padrão. Ele existe como pacote separado, mas o query builder resolve muita coisa.
No começo se digita mais. Em troca, você abre qualquer classe e vê tudo de que ela depende.
Próximo passo
Com isso em mente, o tutorial de CRUD deve parecer uma sequência de passos pequenos e óbvios: uma migration, um repository, um formulário, um controller, as rotas e as views. O código está em yurineves92/crud-yii3.
Se algo aqui não bate com a sua experiência com Yii3, me conta nos comentários. Também estou aprendendo.


Top comments (0)