Valentin Cojocaru Web developer · Cluj-Napoca EN

Valentin
Cojocaru

Construiesc aplicații web și mă ocup de serverul, DNS-ul și baza de date din spatele lor.

blog

Yii2 & PHP 9 min de citit

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.

#Yii2 #PHP #Deployment

Zero to Production: Yii2