Cum structurez un proiect Yii2 Advanced pentru clienți
Când încep un proiect nou pe Yii2 Advanced pentru un client, folosesc o structură care s-a format în câțiva ani și după destule proiecte în care structura de la început a complicat lucrurile degeaba. Iată ce folosesc acum și de ce am luat principalele decizii.
Punctul de plecare nu e șablonul Yii2
Yii2 Advanced îți dă un schelet utilizabil, dar pleacă de la câteva presupuneri care nu se potrivesc în majoritatea proiectelor reale. Primul lucru pe care îl fac e să ajustez structura, înainte să scriu cod de aplicație.
Configurarea mediului cu dotenv
Setup-ul implicit din Yii2 Advanced folosește fișiere de configurare pe mediu, puse în repository. Asta înseamnă că parolele de bază de date ajung în version control, iar trecerea de la un mediu la altul cere atenție la fiecare fișier.
Îl înlocuiesc cu vlucas/phpdotenv:
composer require vlucas/phpdotenv
// common/config/bootstrap.php
$dotenv = DotenvDotenv::createImmutable(dirname(__DIR__, 2));
$dotenv->load();
$dotenv->required(['DB_DSN', 'DB_USERNAME', 'DB_PASSWORD', 'APP_ENV']);
defined('YII_DEBUG') or define('YII_DEBUG', $_ENV['APP_ENV'] === 'development');
defined('YII_ENV') or define('YII_ENV', $_ENV['APP_ENV']);
Fișierele de configurare citesc apoi din $_ENV, în loc să aibă valorile scrise direct. Fișierul .env e în .gitignore, iar .env.example e pus în repository cu toate cheile, dar fără valori sensibile.
Logica de business stă în servicii, nu în controllere
Cea mai frecventă greșeală în proiectele Yii2 sunt controllerele umflate: sute de linii de logică de business amestecate cu tratarea cererii. Un controller ar trebui să facă trei lucruri: validează datele, cheamă un serviciu, întoarce un răspuns.
Directorul meu common/services/ conține logica propriu-zisă:
// common/services/PostService.php
class PostService
{
public function publish(Post $post): void
{
if ($post->status === Post::STATUS_PUBLISHED) {
throw new LogicException("Post is already published.");
}
$post->status = Post::STATUS_PUBLISHED;
$post->published_at = $post->published_at ?? time();
$post->reading_time = $this->calculateReadingTime($post->content);
if (!$post->save()) {
throw new RuntimeException("Failed to save post: " . implode(', ', $post->getFirstErrors()));
}
$this->sitemapService->regenerate();
}
private function calculateReadingTime(string $content): int
{
$wordCount = str_word_count(strip_tags($content));
return (int) ceil($wordCount / 200);
}
}
Controllerul devine simplu:
public function actionPublish(int $id): Response
{
$post = $this->findPost($id);
$this->postService->publish($post);
Yii::$app->session->setFlash('success', 'Post published.');
return $this->redirect(['view', 'id' => $post->id]);
}
Modelele se ocupă doar de date
Modelele definesc schema, regulile de validare, relațiile și comportamentele. Atât. Fără logică de business. Dacă o metodă de pe model ar trebui să cheme alt serviciu sau să facă ceva cu efecte secundare, locul ei e într-o clasă de serviciu.
// Good: data concern, lives in model
public function getIsPublished(): bool
{
return $this->status === self::STATUS_PUBLISHED;
}
// Bad: business concern, should be in a service
public function publish(): void
{
$this->status = self::STATUS_PUBLISHED;
$this->save();
// sending email here is wrong
Yii::$app->mailer->compose()->send();
}
Migrații pentru orice schimbare în baza de date
Orice schimbare de schemă trece printr-o migrație. Fără excepții. Migrațiile rulează în ordine și stau în version control, deci orice developer (sau server) poate reface exact starea bazei de date rulând php yii migrate.
Le dau migrațiilor nume care spun ce fac:
php yii migrate/create add_reading_time_to_post
php yii migrate/create create_analytics_hit_table
php yii migrate/create add_index_to_post_published_at
Structura de directoare care rezultă
common/
config/ - shared config, loaded by all apps
models/ - ActiveRecord models
services/ - business logic
components/ - reusable Yii2 components
mail/ - email view templates
frontend/
controllers/ - thin, request-handling only
views/ - PHP templates
assets/ - asset bundles
web/ - public root (css, js, images)
backend/
controllers/ - admin panel
views/ - admin templates
console/
controllers/ - CLI commands
migrations/ - all DB migrations
De ce merge asta tocmai la proiectele pentru clienți
Proiectele pentru clienți au un ciclu de viață pe care cele interne nu îl au. Sunt predate, reluate după luni de zile, uneori trec la alt developer. O structură previzibilă înseamnă că oricine atinge proiectul data viitoare îl poate înțelege fără să citească tot codul.
Regula mea: dacă nu găsesc un fișier în mai puțin de zece secunde după ce face, proiectul nu e organizat bine. Serviciile țin logica. Controllerele tratează cererile. Modelele definesc datele. Claritatea asta nu costă nimic de întreținut și economisește ore de fiecare dată când cineva are de făcut o schimbare.