Estrutura de diretórios do aplicativo
Como criar uma estrutura de diretórios clara e dimensionável para projetos no Nette Framework? Mostraremos práticas comprovadas que o ajudarão a organizar seu código. Você aprenderá:
- como estruturar logicamente o aplicativo em diretórios
- como projetar a estrutura para escalar bem à medida que o projeto cresce
- quais são as possíveis alternativas e suas vantagens ou desvantagens
É importante mencionar que o Nette Framework em si não insiste em nenhuma estrutura específica. Ele foi projetado para ser facilmente adaptável a quaisquer necessidades e preferências.
Estrutura básica do projeto
Embora o Nette Framework não determine nenhuma estrutura de diretório fixa, há um arranjo padrão comprovado na forma de Projeto Web:
web-project/ ├── app/ ← diretório do aplicativo ├── assets/ ← SCSS, arquivos JS, imagens..., alternativamente resources/ ├── bin/ ← scripts de linha de comando ├── config/ ← configuração ├── log/ ← erros registrados ├── temp/ ← arquivos temporários, cache ├── tests/ ← testes ├── vendor/ ← bibliotecas instaladas pelo Composer └── www/ ← diretório público (raiz do documento)
Você pode modificar livremente essa estrutura de acordo com suas necessidades – renomear ou mover pastas. Em seguida, basta
ajustar os caminhos relativos aos diretórios em Bootstrap.php
e, possivelmente, em composer.json
. Nada
mais é necessário, nenhuma reconfiguração complexa, nenhuma alteração constante. O Nette tem detecção automática
inteligente e reconhece automaticamente o local do aplicativo, incluindo sua base de URL.
Princípios de organização do código
Quando você explora um novo projeto pela primeira vez, deve ser capaz de se orientar rapidamente. Imagine clicar no diretório
app/Model/
e ver esta estrutura:
app/Model/ ├── Services/ ├── Repositories/ └── Entities/
Com isso, você só saberá que o projeto usa alguns serviços, repositórios e entidades. Você não saberá nada sobre a finalidade real do aplicativo.
Vamos dar uma olhada em uma abordagem diferente: organização por domínios:
app/Model/ ├── Cart/ ├── Payment/ ├── Order/ └── Product/
Isso é diferente – à primeira vista, fica claro que se trata de um site de comércio eletrônico. Os próprios nomes dos diretórios revelam o que o aplicativo pode fazer: ele trabalha com pagamentos, pedidos e produtos.
A primeira abordagem (organização por tipo de classe) traz vários problemas na prática: o código que é logicamente relacionado está espalhado em pastas diferentes e você precisa pular entre elas. Portanto, organizaremos por domínios.
Espaços de nome
É convencional que a estrutura de diretórios corresponda aos namespaces no aplicativo. Isso significa que o local físico
dos arquivos corresponde ao namespace deles. Por exemplo, uma classe localizada em
app/Model/Product/ProductRepository.php
deve ter o namespace App\Model\Product
. Esse princípio ajuda
na orientação do código e simplifica o carregamento automático.
Singular vs. Plural em nomes
Observe que usamos o singular para os principais diretórios de aplicativos: app
, config
,
log
, temp
, www
. O mesmo se aplica dentro do aplicativo: Model
,
Core
, Presentation
. Isso ocorre porque cada um representa um conceito unificado.
Da mesma forma, app/Model/Product
representa tudo sobre produtos. Não o chamamos de Products
porque
não se trata de uma pasta cheia de produtos (que conteria arquivos como iphone.php
, samsung.php
). É um
namespace que contém classes para trabalhar com produtos – ProductRepository.php
,
ProductService.php
.
A pasta app/Tasks
está no plural porque contém um conjunto de scripts executáveis autônomos –
CleanupTask.php
, ImportTask.php
. Cada um deles é uma unidade independente.
Para fins de consistência, recomendamos o uso de:
- Singular para namespaces que representam uma unidade funcional (mesmo se estiver trabalhando com várias entidades)
- Plural para coleções de unidades independentes
- Em caso de incerteza ou se você não quiser pensar sobre isso, escolha singular
Diretório público www/
Esse diretório é o único acessível pela Web (o chamado document-root). É possível que você encontre com frequência
o nome public/
em vez de www/
– é apenas uma questão de convenção e não afeta a funcionalidade.
O diretório contém:
- Ponto de entrada do aplicativo
index.php
- Arquivo
.htaccess
com regras mod_rewrite (para Apache) - Arquivos estáticos (CSS, JavaScript, imagens)
- Arquivos carregados
Para garantir a segurança adequada do aplicativo, é fundamental ter o document-root configurado corretamente.
Nunca coloque a pasta node_modules/
nesse diretório, pois ela contém milhares de arquivos que podem
ser executáveis e não devem ser acessíveis ao público.
Diretório de aplicativos app/
Este é o diretório principal com o código do aplicativo. Estrutura básica:
app/ ├── Core/ ← questões de infraestrutura ├── Model/ ← lógica de negócios ├── Presentation/ ← apresentadores e modelos ├── Tasks/ ← scripts de comando └── Bootstrap.php ← classe bootstrap do aplicativo
Bootstrap.php
é a classe de inicialização do
aplicativo que inicializa o ambiente, carrega a configuração e cria o contêiner DI.
Vamos agora examinar detalhadamente os subdiretórios individuais.
Apresentadores e modelos
Temos a parte de apresentação do aplicativo no diretório app/Presentation
. Uma alternativa é o curto
app/UI
. Esse é o local para todos os apresentadores, seus modelos e todas as classes auxiliares.
Organizamos essa camada por domínios. Em um projeto complexo que combina comércio eletrônico, blog e API, a estrutura seria semelhante a esta:
app/Presentation/ ├── Shop/ ← front-end de comércio eletrônico │ ├── Product/ │ ├── Cart/ │ └── Order/ ├── Blog/ ← blog │ ├── Home/ │ └── Post/ ├── Admin/ ← administração │ ├── Dashboard/ │ └── Products/ └── Api/ ← Pontos de extremidade da API └── V1/
Por outro lado, para um blog simples, usaríamos esta estrutura:
app/Presentation/ ├── Front/ ← front-end do site │ ├── Home/ │ └── Post/ ├── Admin/ ← administração │ ├── Dashboard/ │ └── Posts/ ├── Error/ └── Export/ ← RSS, sitemaps etc.
Pastas como Home/
ou Dashboard/
contêm apresentadores e modelos. Pastas como Front/
,
Admin/
ou Api/
são chamadas de módulos. Tecnicamente, esses são diretórios regulares que
servem para a organização lógica do aplicativo.
Cada pasta com um apresentador contém um apresentador com o mesmo nome e seus modelos. Por exemplo, a pasta
Dashboard/
contém:
Dashboard/ ├── DashboardPresenter.php ← apresentador └── default.latte ← modelo
Essa estrutura de diretório é refletida nos namespaces de classe. Por exemplo, DashboardPresenter
está no
namespace App\Presentation\Admin\Dashboard
(consulte o mapeamento do
apresentador):
namespace App\Presentation\Admin\Dashboard;
class DashboardPresenter extends Nette\Application\UI\Presenter
{
//...
}
Referimo-nos ao apresentador Dashboard
dentro do módulo Admin
no aplicativo usando a notação de
dois pontos como Admin:Dashboard
. Para sua ação default
, então, como
Admin:Dashboard:default
. Para módulos aninhados, usamos mais dois-pontos, por exemplo,
Shop:Order:Detail:default
.
Desenvolvimento de estrutura flexível
Uma das grandes vantagens dessa estrutura é a elegância com que ela se adapta às necessidades crescentes do projeto. Como exemplo, vamos pegar a parte que gera feeds XML. Inicialmente, temos um formulário simples:
Export/ ├── ExportPresenter.php ← um apresentador para todas as exportações ├── sitemap.latte ← modelo para mapa do site └── feed.latte ← modelo para feed RSS
Com o tempo, mais tipos de feed são adicionados e precisamos de mais lógica para eles… Sem problemas! A pasta
Export/
simplesmente se torna um módulo:
Export/ ├── Sitemap/ │ ├── SitemapPresenter.php │ └── sitemap.latte └── Feed/ ├── FeedPresenter.php ├── amazon.latte ← feed para a Amazon └── ebay.latte ← feed para eBay
Essa transformação é totalmente tranquila – basta criar novas subpastas, dividir o código entre elas e atualizar os
links (por exemplo, de Export:feed
para Export:Feed:amazon
). Graças a isso, podemos expandir
gradualmente a estrutura conforme necessário, pois o nível de aninhamento não é limitado de forma alguma.
Por exemplo, se na administração você tiver muitos apresentadores relacionados ao gerenciamento de pedidos, como
OrderDetail
, OrderEdit
, OrderDispatch
etc., poderá criar um módulo (pasta)
Order
para melhor organização, que conterá (pastas para) apresentadores Detail
, Edit
,
Dispatch
e outros.
Localização do modelo
Nos exemplos anteriores, vimos que os modelos estão localizados diretamente na pasta com o apresentador:
Dashboard/ ├── DashboardPresenter.php ← apresentador ├── DashboardTemplate.php ← classe de modelo opcional └── default.latte ← modelo
Esse local se mostra o mais conveniente na prática: você tem todos os arquivos relacionados à mão.
Como alternativa, você pode colocar os modelos em uma subpasta templates/
. O Nette oferece suporte a ambas as
variantes. Você pode até mesmo colocar os modelos completamente fora da pasta Presentation/
. Tudo sobre as opções
de localização de modelos pode ser encontrado no capítulo Pesquisa de modelos.
Classes e componentes auxiliares
Os apresentadores e modelos geralmente vêm com outros arquivos auxiliares. Nós os colocamos logicamente de acordo com seu escopo:
1. Diretamente com o apresentador no caso de componentes específicos para um determinado apresentador:
Product/ ├── ProductPresenter.php ├── ProductGrid.php ← componente para listagem de produtos └── FilterForm.php ← formulário para filtragem
2. Para o módulo – recomendamos o uso da pasta Accessory
, que é colocada ordenadamente no início
do alfabeto:
Front/ ├── Accessory/ │ ├── NavbarControl.php ← componentes para front-end │ └── TemplateFilters.php ├── Product/ └── Cart/
3. Para todo o aplicativo – em Presentation/Accessory/
:
app/Presentation/ ├── Accessory/ │ ├── LatteExtension.php │ └── TemplateFilters.php ├── Front/ └── Admin/
Ou você pode colocar classes auxiliares como LatteExtension.php
ou TemplateFilters.php
na pasta de
infraestrutura app/Core/Latte/
. E os componentes em app/Components
. A escolha depende das convenções
da equipe.
Modelo – Coração do aplicativo
O modelo contém toda a lógica comercial do aplicativo. Para sua organização, aplica-se a mesma regra: estruturamos por domínios:
app/Model/ ├── Payment/ ← tudo sobre pagamentos │ ├── PaymentFacade.php ← ponto de entrada principal │ ├── PaymentRepository.php │ ├── Payment.php ← entidade ├── Order/ ← tudo sobre pedidos │ ├── OrderFacade.php │ ├── OrderRepository.php │ ├── Order.php └── Shipping/ ← tudo sobre remessa
No modelo, você normalmente encontra esses tipos de classes:
Facades: representam o principal ponto de entrada em um domínio específico no aplicativo. Elas atuam como um orquestrador que coordena a cooperação entre diferentes serviços para implementar casos de uso completos (como “criar pedido” ou “processar pagamento”). Sob sua camada de orquestração, a fachada oculta os detalhes de implementação do restante do aplicativo, fornecendo assim uma interface limpa para trabalhar com o domínio em questão.
class OrderFacade
{
public function createOrder(Cart $cart): Order
{
// validação
// criação de pedidos
// envio de e-mails
// gravação em estatísticas
}
}
Serviços: concentram-se em operações comerciais específicas em um domínio. Ao contrário das fachadas que orquestram casos de uso inteiros, um serviço implementa uma lógica comercial específica (como cálculos de preços ou processamento de pagamentos). Os serviços normalmente não têm estado e podem ser usados por fachadas como blocos de construção para operações mais complexas ou diretamente por outras partes do aplicativo para tarefas mais simples.
class PricingService
{
public function calculateTotal(Order $order): Money
{
// Cálculo de preços
}
}
Repositórios: lidam com toda a comunicação com o armazenamento de dados, normalmente um banco de dados. Sua tarefa é carregar e salvar entidades e implementar métodos para pesquisá-las. Um repositório protege o restante do aplicativo dos detalhes de implementação do banco de dados e fornece uma interface orientada a objetos para trabalhar com dados.
class OrderRepository
{
public function find(int $id): ?Order
{
}
public function findByCustomer(int $customerId): array
{
}
}
Entidades: objetos que representam os principais conceitos de negócios no aplicativo que têm sua identidade e mudam com o tempo. Normalmente, são classes mapeadas para tabelas de banco de dados usando ORM (como Nette Database Explorer ou Doctrine). As entidades podem conter regras de negócios relacionadas aos seus dados e à lógica de validação.
// Entidade mapeada para a tabela de pedidos do banco de dados
class Order extends Nette\Database\Table\ActiveRow
{
public function addItem(Product $product, int $quantity): void
{
$this->related('order_items')->insert([
'product_id' => $product->id,
'quantity' => $quantity,
'unit_price' => $product->price,
]);
}
}
Objetos de valor: objetos imutáveis que representam valores sem identidade própria – por exemplo, uma quantia em dinheiro ou um endereço de e-mail. Duas instâncias de um objeto de valor com os mesmos valores são consideradas idênticas.
Código de infraestrutura
A pasta Core/
(ou também Infrastructure/
) abriga a base técnica do aplicativo. O código de
infraestrutura normalmente inclui:
app/Core/ ├── Router/ ← roteamento e gerenciamento de URLs │ └── RouterFactory.php ├── Security/ ← autenticação e autorização │ ├── Authenticator.php │ └── Authorizator.php ├── Logging/ ← registro e monitoramento │ ├── SentryLogger.php │ └── FileLogger.php ├── Cache/ ← camada de cache │ └── FullPageCache.php └── Integration/ ← integração com serviços externos ├── Slack/ └── Stripe/
Para projetos menores, uma estrutura plana é naturalmente suficiente:
Core/ ├── RouterFactory.php ├── Authenticator.php └── QueueMailer.php
Este é o código que:
- Lida com a infraestrutura técnica (roteamento, registro, armazenamento em cache)
- Integra serviços externos (Sentry, Elasticsearch, Redis)
- Fornece serviços básicos para todo o aplicativo (correio, banco de dados)
- É, em grande parte, independente do domínio específico – o cache ou o registrador funciona da mesma forma para comércio eletrônico ou blog.
Está se perguntando se uma determinada classe pertence a este site ou ao modelo? A principal diferença é que o código em
Core/
:
- Não sabe nada sobre o domínio (produtos, pedidos, artigos)
- Geralmente pode ser transferido para outro projeto
- Resolve “como funciona” (como enviar e-mail), não “o que faz” (que e-mail enviar)
Exemplo para melhor compreensão:
App\Core\MailerFactory
– cria instâncias da classe de envio de e-mail, lida com as configurações de SMTPApp\Model\OrderMailer
– usaMailerFactory
para enviar e-mails sobre pedidos, conhece seus modelos e quando eles devem ser enviados
Scripts de comando
Os aplicativos geralmente precisam executar tarefas fora das solicitações HTTP regulares, seja processamento de dados em
segundo plano, manutenção ou tarefas periódicas. Scripts simples no diretório bin/
são usados para execução,
enquanto a lógica de implementação real é colocada em app/Tasks/
(ou app/Commands/
).
Exemplo:
app/Tasks/ ├── Maintenance/ ← scripts de manutenção │ ├── CleanupCommand.php ← exclusão de dados antigos │ └── DbOptimizeCommand.php ← otimização do banco de dados ├── Integration/ ← integração com sistemas externos │ ├── ImportProducts.php ← importação do sistema do fornecedor │ └── SyncOrders.php ← sincronização de pedidos └── Scheduled/ ← tarefas regulares ├── NewsletterCommand.php ← envio de boletins informativos └── ReminderCommand.php ← notificações de clientes
O que pertence ao modelo e o que pertence aos scripts de comando? Por exemplo, a lógica para enviar um e-mail faz parte do
modelo, enquanto o envio em massa de milhares de e-mails pertence a Tasks/
.
As tarefas geralmente são executadas na linha de
comando ou via cron. Elas também podem ser executadas por meio de solicitação HTTP, mas a segurança deve ser considerada.
O apresentador que executa a tarefa precisa ser protegido, por exemplo, somente para usuários conectados ou com um token forte e
acesso a partir de endereços IP permitidos. Para tarefas longas, é necessário aumentar o limite de tempo do script e usar
session_write_close()
para evitar o bloqueio da sessão.
Outros diretórios possíveis
Além dos diretórios básicos mencionados, você pode adicionar outras pastas especializadas de acordo com as necessidades do projeto. Vamos dar uma olhada nas mais comuns e em seu uso:
app/ ├── Api/ ← Lógica de API independente da camada de apresentação ├── Database/ ← scripts de migração e seeders para dados de teste ├── Components/ ← componentes visuais compartilhados em todo o aplicativo ├── Event/ ← útil se estiver usando arquitetura orientada por eventos ├── Mail/ ← modelos de e-mail e lógica relacionada └── Utils/ ← classes auxiliares
Para componentes visuais compartilhados usados em apresentadores em todo o aplicativo, você pode usar a pasta
app/Components
ou app/Controls
:
app/Components/ ├── Form/ ← componentes de formulários compartilhados │ ├── SignInForm.php │ └── UserForm.php ├── Grid/ ← componentes para listagens de dados │ └── DataGrid.php └── Navigation/ ← elementos de navegação ├── Breadcrumbs.php └── Menu.php
É a essa pasta que pertencem os componentes com lógica mais complexa. Se quiser compartilhar componentes entre vários projetos, é bom separá-los em um pacote autônomo do composer.
No diretório app/Mail
, você pode colocar o gerenciamento de comunicação por e-mail:
app/Mail/ ├── templates/ ← modelos de e-mail │ ├── order-confirmation.latte │ └── welcome.latte └── OrderMailer.php
Mapeamento de apresentadores
O mapeamento define regras para derivar nomes de classes de nomes de apresentadores. Nós as especificamos na configuração sob a chave application › mapping
.
Nesta página, mostramos que colocamos os apresentadores na pasta app/Presentation
(ou app/UI
).
Precisamos informar ao Nette sobre essa convenção no arquivo de configuração. Uma linha é suficiente:
application:
mapping: App\Presentation\*\**Presenter
Como funciona o mapeamento? Para entender melhor, vamos primeiro imaginar um aplicativo sem módulos. Queremos que as classes
de apresentador fiquem sob o namespace App\Presentation
, de modo que o apresentador Home
seja mapeado
para a classe App\Presentation\HomePresenter
. Isso é obtido com esta configuração:
application:
mapping: App\Presentation\*Presenter
O mapeamento funciona substituindo o asterisco na máscara App\Presentation\*Presenter
pelo nome do apresentador
Home
, resultando no nome final da classe App\Presentation\HomePresenter
. Simples!
Entretanto, como você pode ver nos exemplos deste e de outros capítulos, colocamos as classes de apresentador em
subdiretórios homônimos, por exemplo, o apresentador Home
mapeia para a classe
App\Presentation\Home\HomePresenter
. Conseguimos isso duplicando os dois pontos (requer o Nette
Application 3.2):
application:
mapping: App\Presentation\**Presenter
Agora, passaremos a mapear os apresentadores nos módulos. Podemos definir um mapeamento específico para cada módulo:
application:
mapping:
Front: App\Presentation\Front\**Presenter
Admin: App\Presentation\Admin\**Presenter
Api: App\Api\*Presenter
De acordo com essa configuração, o apresentador Front:Home
mapeia para a classe
App\Presentation\Front\Home\HomePresenter
, enquanto o apresentador Api:OAuth
mapeia para a classe
App\Api\OAuthPresenter
.
Como os módulos Front
e Admin
têm um método de mapeamento semelhante e provavelmente haverá mais
módulos desse tipo, é possível criar uma regra geral que os substituirá. Um novo asterisco para o módulo será adicionado à
máscara de classe:
application:
mapping:
*: App\Presentation\*\**Presenter
Api: App\Api\*Presenter
Isso também funciona para estruturas de diretório aninhadas mais profundas, como o apresentador
Admin:User:Edit
, em que o segmento com asterisco se repete para cada nível e resulta na classe
App\Presentation\Admin\User\Edit\EditPresenter
.
Uma notação alternativa é usar uma matriz composta por três segmentos em vez de uma cadeia de caracteres. Essa notação é equivalente à anterior:
application:
mapping:
*: [App\Presentation, *, **Presenter]
Api: [App\Api, '', *Presenter]