Estrutura de diretórios da aplicação

Como projetar uma estrutura de diretórios clara e escalável para projetos no Nette Framework? Mostraremos as melhores práticas que o ajudarão a organizar seu código. Você aprenderá:

  • como dividir logicamente a aplicação em diretórios
  • como projetar a estrutura para que ela escale bem com o crescimento do projeto
  • quais são as alternativas possíveis e suas vantagens ou desvantagens

É importante mencionar que o próprio Nette Framework não impõe nenhuma estrutura específica. Ele é projetado para ser facilmente adaptável a quaisquer necessidades e preferências.

Estrutura básica do projeto

Embora o Nette Framework não dite nenhuma estrutura de diretórios fixa, existe uma organização padrão comprovada na forma do Web Project:

web-project/
├── app/              ← diretório com a aplicação
├── assets/           ← arquivos SCSS, JS, imagens..., alternativamente resources/
├── bin/              ← scripts para a 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 (document-root)

Você pode modificar esta estrutura livremente de acordo com suas necessidades – renomear ou mover pastas. Depois, basta apenas ajustar os caminhos relativos aos diretórios no arquivo Bootstrap.php e, opcionalmente, composer.json. Nada mais é necessário, nenhuma reconfiguração complicada, nenhuma alteração de constantes. O Nette possui uma autodeteção inteligente e reconhece automaticamente a localização da aplicação, incluindo sua base de URL.

Princípios de organização do código

Quando você explora um novo projeto pela primeira vez, deve conseguir se orientar rapidamente nele. Imagine que você abre o diretório app/Model/ e vê esta estrutura:

app/Model/
├── Services/
├── Repositories/
└── Entities/

A partir dela, você só pode deduzir que o projeto usa alguns serviços, repositórios e entidades. Você não aprenderá nada sobre o propósito real da aplicação.

Vejamos outra abordagem – organização por domínios:

app/Model/
├── Cart/
├── Payment/
├── Order/
└── Product/

Aqui é diferente – à primeira vista, fica claro que se trata de uma loja virtual. Os próprios nomes dos diretórios revelam o que a aplicação faz – trabalha com pagamentos, pedidos e produtos.

A primeira abordagem (organização por tipo de classe) traz na prática uma série de problemas: o código que está logicamente relacionado é fragmentado em diferentes pastas e você precisa pular entre elas. Portanto, organizaremos por domínios.

Namespaces

É costume que a estrutura de diretórios corresponda aos namespaces na aplicação. Isso significa que a localização física dos arquivos corresponde ao seu namespace. Por exemplo, uma classe localizada em app/Model/Product/ProductRepository.php deve ter o namespace App\Model\Product. Este princípio ajuda na orientação no código e simplifica o autoloading.

Singular vs. plural nos nomes

Observe que para os diretórios principais da aplicação usamos o singular: app, config, log, temp, www. O mesmo vale para o interior da aplicação: Model, Core, Presentation. Isso ocorre porque cada um deles representa um conceito coeso.

Da mesma forma, por exemplo, app/Model/Product representa tudo relacionado a produtos. Não o chamaremos de Products, porque não é uma pasta cheia de produtos (isso significaria que haveria arquivos nokia.php, samsung.php). É um namespace contendo 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 independentes – CleanupTask.php, ImportTask.php. Cada um deles é uma unidade separada.

Para consistência, recomendamos usar:

  • Singular para namespaces que representam uma unidade funcional (mesmo que trabalhe com múltiplas entidades)
  • Plural para coleções de unidades independentes
  • Em caso de incerteza ou se você não quiser pensar sobre isso, escolha o singular

Diretório público www/

Este diretório é o único acessível pela web (o chamado document-root). Frequentemente, você pode encontrar o nome public/ em vez de www/ – é apenas uma questão de convenção e não afeta a funcionalidade do Nette. O diretório contém:

  • Ponto de entrada da aplicação index.php
  • Arquivo .htaccess com regras para mod_rewrite (no Apache)
  • Arquivos estáticos (CSS, JavaScript, imagens)
  • Arquivos carregados (uploads)

Para a segurança adequada da aplicação, é crucial ter o document-root configurado corretamente.

Nunca coloque a pasta node_modules/ neste diretório – ela contém milhares de arquivos que podem ser executáveis e não devem estar publicamente acessíveis.

Diretório da aplicação app/

Este é o diretório principal com o código da aplicação. Estrutura básica:

app/
├── Core/               ← questões de infraestrutura
├── Model/              ← lógica de negócios
├── Presentation/       ← presenters e templates
├── Tasks/              ← scripts de comando
└── Bootstrap.php       ← classe de inicialização da aplicação

Bootstrap.php é a classe de inicialização da aplicação, que inicializa o ambiente, carrega a configuração e cria o contêiner de DI.

Vamos agora examinar os subdiretórios individuais com mais detalhes.

Presenters e templates

A parte de apresentação da aplicação está no diretório app/Presentation. Uma alternativa é o curto app/UI. É o local para todos os presenters, seus templates e quaisquer classes auxiliares.

Organizamos esta camada por domínios. Em um projeto complexo que combina uma loja virtual, um blog e uma API, a estrutura seria assim:

app/Presentation/
├── Shop/              ← frontend da loja virtual
│   ├── Product/
│   ├── Cart/
│   └── Order/
├── Blog/              ← blog
│   ├── Home/
│   └── Post/
├── Admin/             ← administração
│   ├── Dashboard/
│   └── Products/
└── Api/               ← endpoints da API
	└── V1/

Por outro lado, para um blog simples, usaríamos a seguinte divisão:

app/Presentation/
├── Front/             ← frontend do site
│   ├── Home/
│   └── Post/
├── Admin/             ← administração
│   ├── Dashboard/
│   └── Posts/
├── Error/
└── Export/            ← RSS, sitemaps, etc.

Pastas como Home/ ou Dashboard/ contêm presenters e templates. Pastas como Front/, Admin/ ou Api/ são chamadas de módulos. Tecnicamente, são diretórios comuns que servem para a divisão lógica da aplicação.

Cada pasta com um presenter contém um presenter de mesmo nome e seus templates. Por exemplo, a pasta Dashboard/ contém:

Dashboard/
├── DashboardPresenter.php     ← presenter
└── default.latte              ← template

Esta estrutura de diretórios se reflete nos namespaces das classes. Por exemplo, DashboardPresenter está localizado no namespace App\Presentation\Admin\Dashboard (veja mapování presenterů):

namespace App\Presentation\Admin\Dashboard;

class DashboardPresenter extends Nette\Application\UI\Presenter
{
	// ...
}

Referimo-nos ao presenter Dashboard dentro do módulo Admin na aplicação usando a notação de dois pontos como Admin:Dashboard. À sua ação default, então, como Admin:Dashboard:default. No caso de módulos aninhados, usamos mais dois pontos, por exemplo, Shop:Order:Detail:default.

Desenvolvimento flexível da estrutura

Uma das grandes vantagens desta estrutura é como ela se adapta elegantemente às necessidades crescentes do projeto. Como exemplo, vejamos a parte que gera feeds XML. No início, temos uma forma simples:

Export/
├── ExportPresenter.php   ← um presenter para todas as exportações
├── sitemap.latte         ← template para o sitemap
└── feed.latte            ← template para o feed RSS

Com o tempo, mais tipos de feeds 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
	├── zbozi.latte         ← feed para Zboží.cz
	└── heureka.latte       ← feed para Heureka.cz

Esta transformação é completamente fluida – basta criar novas subpastas, dividir o código nelas e atualizar os links (por exemplo, de Export:feed para Export:Feed:zbozi). Graças a isso, podemos expandir gradualmente a estrutura conforme necessário, o nível de aninhamento não é limitado de forma alguma.

Se, por exemplo, na administração você tiver muitos presenters relacionados ao gerenciamento de pedidos, como OrderDetail, OrderEdit, OrderDispatch, etc., você pode criar um módulo (pasta) Order neste local para melhor organização, que conterá (pastas para) os presenters Detail, Edit, Dispatch e outros.

Localização dos templates

Nos exemplos anteriores, vimos que os templates estão localizados diretamente na pasta com o presenter:

Dashboard/
├── DashboardPresenter.php     ← presenter
├── DashboardTemplate.php      ← classe opcional para o template
└── default.latte              ← template

Esta localização se mostra na prática a mais conveniente – todos os arquivos relacionados estão à mão.

Alternativamente, você pode colocar os templates em uma subpasta templates/. O Nette suporta ambas as variantes. Você pode até colocar os templates completamente fora da pasta Presentation/. Tudo sobre as opções de localização de templates pode ser encontrado no capítulo Procurando templates.

Classes auxiliares e componentes

Frequentemente, presenters e templates são acompanhados por outros arquivos auxiliares. Nós os colocamos logicamente de acordo com seu escopo:

1. Diretamente com o presenter no caso de componentes específicos para esse presenter:

Product/
├── ProductPresenter.php
├── ProductGrid.php        ← componente para listagem de produtos
└── FilterForm.php         ← formulário para filtragem

2. Para o módulo – recomendamos usar a pasta Accessory, que é colocada de forma clara no início do alfabeto:

Front/
├── Accessory/
│   ├── NavbarControl.php    ← componentes para o frontend
│   └── TemplateFilters.php
├── Product/
└── Cart/

3. Para toda a aplicação – 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 componentes em app/Components. A escolha depende dos costumes da equipe.

Model – o coração da aplicação

O Model contém toda a lógica de negócios da aplicação. Para sua organização, a regra se aplica novamente – 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 envio

No model, você normalmente encontrará estes tipos de classes:

Facades: representam o ponto de entrada principal para um domínio específico na aplicação. 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 facade esconde os detalhes de implementação do resto da aplicação, fornecendo assim uma interface limpa para trabalhar com o domínio dado.

class OrderFacade
{
	public function createOrder(Cart $cart): Order
	{
		// validação
		// criação do pedido
		// envio de e-mail
		// registro nas estatísticas
	}
}

Serviços: focam em uma operação de negócios específica dentro do domínio. Ao contrário da facade, que orquestra casos de uso inteiros, um serviço implementa lógica de negócios específica (como cálculos de preços ou processamento de pagamentos). Os serviços são tipicamente sem estado e podem ser usados por facades como blocos de construção para operações mais complexas, ou diretamente por outras partes da aplicação para tarefas mais simples.

class PricingService
{
	public function calculateTotal(Order $order): Money
	{
		// cálculo do preço
	}
}

Repositórios: garantem toda a comunicação com o armazenamento de dados, tipicamente um banco de dados. Sua tarefa é carregar e salvar entidades e implementar métodos para sua busca. O repositório isola o resto da aplicação 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 na aplicação, que têm sua identidade e mudam ao longo do tempo. Tipicamente, 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 banco de dados orders
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,
		]);
	}
}

Value objects: objetos imutáveis que representam valores sem identidade própria – por exemplo, um valor monetário ou um endereço de e-mail. Duas instâncias de um value object com os mesmos valores são consideradas idênticas.

Código de infraestrutura

A pasta Core/ (ou também Infrastructure/) é o lar da base técnica da aplicação. O código de infraestrutura normalmente inclui:

app/Core/
├── Router/               ← roteamento e gerenciamento de URL
│   └── RouterFactory.php
├── Security/             ← autenticação e autorização
│   ├── Authenticator.php
│   └── Authorizator.php
├── Logging/              ← logging e monitoramento
│   ├── SentryLogger.php
│   └── FileLogger.php
├── Cache/                ← camada de cache
│   └── FullPageCache.php
└── Integration/          ← integração com serviços ext.
	├── Slack/
	└── Stripe/

Para projetos menores, uma estrutura plana é obviamente suficiente:

Core/
├── RouterFactory.php
├── Authenticator.php
└── QueueMailer.php

É o código que:

  • Lida com a infraestrutura técnica (roteamento, logging, cache)
  • Integra serviços externos (Sentry, Elasticsearch, Redis)
  • Fornece serviços básicos para toda a aplicação (e-mail, banco de dados)
  • É geralmente independente do domínio específico – cache ou logger funciona da mesma forma para uma loja virtual ou blog.

Está em dúvida se uma determinada classe pertence aqui ou ao model? A diferença crucial é que o código em Core/:

  • Não sabe nada sobre o domínio (produtos, pedidos, artigos)
  • Geralmente pode ser transferido para outro projeto
  • Lida com “como funciona” (como enviar um e-mail), não “o que faz” (qual e-mail enviar)

Exemplo para melhor compreensão:

  • App\Core\MailerFactory – cria instâncias da classe para envio de e-mails, lida com configurações SMTP
  • App\Model\OrderMailer – usa MailerFactory para enviar e-mails sobre pedidos, conhece seus templates e sabe quando devem ser enviados

Scripts de comando

Aplicações frequentemente precisam executar atividades fora das requisições HTTP normais – 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 é 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 agendadas
	├── NewsletterCommand.php  ← envio de newsletters
	└── ReminderCommand.php    ← notificações para clientes

O que pertence ao model e o que pertence aos scripts de comando? Por exemplo, a lógica para enviar um único e-mail faz parte do model, o envio em massa de milhares de e-mails já pertence a Tasks/.

As tarefas são geralmente executadas a partir da linha de comando ou via cron. Elas também podem ser executadas via requisição HTTP, mas é necessário pensar na segurança. O presenter que inicia a tarefa precisa ser protegido, por exemplo, apenas para usuários logados ou com um token forte e acesso de endereços IP permitidos. Para tarefas longas, é necessário aumentar o limite de tempo do script e usar session_write_close() para não bloquear a 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. Vejamos as mais comuns e seus usos:

app/
├── Api/              ← lógica para API independente da camada de apresentação
├── Database/         ← scripts de migração e seeders para dados de teste
├── Components/       ← componentes visuais compartilhados em toda a aplicação
├── Event/            ← útil se você usa arquitetura orientada a eventos
├── Mail/             ← templates de e-mail e lógica relacionada
└── Utils/            ← classes auxiliares

Para componentes visuais compartilhados usados em presenters em toda a aplicação, a pasta app/Components ou app/Controls pode ser usada:

app/Components/
├── Form/                 ← componentes de formulário compartilhados
│   ├── SignInForm.php
│   └── UserForm.php
├── Grid/                 ← componentes para listagens de dados
│   └── DataGrid.php
└── Navigation/           ← elementos de navegação
	├── Breadcrumbs.php
	└── Menu.php

Aqui pertencem componentes que têm lógica mais complexa. Se você deseja compartilhar componentes entre vários projetos, é aconselhável extraí-los para um pacote composer separado.

No diretório app/Mail, você pode colocar o gerenciamento da comunicação por e-mail:

app/Mail/
├── templates/            ← templates de e-mail
│   ├── order-confirmation.latte
│   └── welcome.latte
└── OrderMailer.php

Mapeamento de presenters

O mapeamento define regras para derivar o nome da classe a partir do nome do presenter. Especificamo-las na configuração sob a chave application › mapping.

Nesta página, mostramos que colocamos os presenters na pasta app/Presentation (ou app/UI). Precisamos informar esta convenção ao Nette no arquivo de configuração. Basta uma linha:

application:
	mapping: App\Presentation\*\**Presenter

Como funciona o mapeamento? Para melhor compreensão, imaginemos primeiro uma aplicação sem módulos. Queremos que as classes dos presenters caiam no namespace App\Presentation, para que o presenter Home seja mapeado para a classe App\Presentation\HomePresenter. O que conseguimos com esta configuração:

application:
	mapping: App\Presentation\*Presenter

O mapeamento funciona de forma que o nome do presenter Home substitui o asterisco na máscara App\Presentation\*Presenter, resultando no nome final da classe App\Presentation\HomePresenter. Simples!

Mas, como você pode ver nos exemplos neste e em outros capítulos, colocamos as classes dos presenters em subdiretórios homônimos, por exemplo, o presenter Home é mapeado para a classe App\Presentation\Home\HomePresenter. Conseguimos isso duplicando os dois pontos (requer Nette Application 3.2):

application:
	mapping: App\Presentation\**Presenter

Agora vamos mapear presenters para módulos. Para cada módulo, podemos definir um mapeamento específico:

application:
	mapping:
		Front: App\Presentation\Front\**Presenter
		Admin: App\Presentation\Admin\**Presenter
		Api: App\Api\*Presenter

De acordo com esta configuração, o presenter Front:Home é mapeado para a classe App\Presentation\Front\Home\HomePresenter, enquanto o presenter Api:OAuth 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 assim, é possível criar uma regra geral que os substitua. Um novo asterisco para o módulo é adicionado à máscara da classe:

application:
	mapping:
		*: App\Presentation\*\**Presenter
		Api: App\Api\*Presenter

Isso também funciona para estruturas de diretórios mais profundamente aninhadas, como, por exemplo, o presenter Admin:User:Edit, o segmento com asterisco se repete para cada nível e o resultado é a classe App\Presentation\Admin\User\Edit\EditPresenter.

Uma notação alternativa é usar um array composto por três segmentos em vez de uma string. Esta notação é equivalente à anterior:

application:
	mapping:
		*: [App\Presentation, *, **Presenter]
		Api: [App\Api, '', *Presenter]
versão: 4.0