diff --git a/src/.vitepress/config.js b/src/.vitepress/config.js index 1cd9fd08..49541f51 100644 --- a/src/.vitepress/config.js +++ b/src/.vitepress/config.js @@ -157,6 +157,7 @@ export default { text: 'Cookbook', items: [ {text: 'Preface', link: '/cookbook/preface'}, + {text: 'Implementing Advanced App Structure', link: '/cookbook/replicating-yii2-advanced-app-structure'}, {text: 'Making HTTP Requests', link: '/cookbook/making-http-requests'}, {text: 'Disabling CSRF Protection', link: '/cookbook/disabling-csrf-protection'}, {text: 'Sentry Integration', link: '/cookbook/sentry-integration'} diff --git a/src/cookbook/index.md b/src/cookbook/index.md index a16f5e03..ae970664 100644 --- a/src/cookbook/index.md +++ b/src/cookbook/index.md @@ -13,6 +13,7 @@ This book conforms to the [Terms of Yii Documentation](https://www.yiiframework. --- - [Preface](preface.md) +- [Implementing advanced app structure](replicating-yii2-advanced-app-structure.md) - [Structuring code by use-case with vertical slices](organizing-code/structuring-by-use-case-with-vertical-slices.md) - [Making HTTP requests](making-http-requests.md) - [Disabling CSRF protection](disabling-csrf-protection.md) diff --git a/src/cookbook/replicating-yii2-advanced-app-structure.md b/src/cookbook/replicating-yii2-advanced-app-structure.md new file mode 100644 index 00000000..632107cd --- /dev/null +++ b/src/cookbook/replicating-yii2-advanced-app-structure.md @@ -0,0 +1,1039 @@ +# Implementing advanced app structure with Yii3 + +This recipe shows how to structure a Yii3 application with multiple entry points for frontend, backend, console, and API applications, similar to the advanced application template pattern. + +## Application structure + +The structure uses multiple entry points with separate configurations for each application section: + +``` +yii3-app/ +├── config/ # Configuration files +│ ├── common/ # Shared configuration +│ ├── web/ # Frontend application config +│ ├── admin/ # Backend application config +│ ├── api/ # API application config +│ └── console/ # Console application config +├── public/ # Web root with entry scripts +│ ├── index.php # Frontend entry point +│ ├── admin.php # Backend entry point +│ └── api.php # API entry point +├── src/ # Application source code +│ ├── Frontend/ # Frontend-specific code +│ ├── Admin/ # Backend-specific code +│ ├── Api/ # API-specific code +│ ├── Console/ # Console-specific code +│ └── Shared/ # Shared code between applications +├── resources/ # Views, assets, translations +│ ├── views/ +│ │ ├── frontend/ +│ │ └── admin/ +│ └── assets/ +│ ├── frontend/ +│ └── admin/ +└── vendor/ +``` + +This structure provides: + +1. **Multiple entry points** - Separate entry scripts for each application +2. **Configuration-based separation** - Each application has its own config +3. **Shared code organization** - Common code in a dedicated namespace +4. **Middleware-based logic** - Different middleware stacks per application + +## Setting up multiple entry points + +### Creating separate entry scripts + +To replicate the frontend/backend separation, create multiple entry points in your `public/` directory: + +#### Frontend entry script + +Create `public/index.php` for your public-facing application: + +```php +run(); +``` + +#### Backend entry script + +Create `public/admin.php` for your administration panel: + +```php +run(); +``` + +### Configuring web server routing + +Configure your web server to route requests appropriately: + +#### Nginx configuration + +```nginx +server { + listen 80; + server_name example.com; + root /path/to/app/public; + index index.php; + + # Frontend application + location / { + try_files $uri $uri/ /index.php$is_args$args; + } + + # Backend application (admin panel) + location /admin { + try_files $uri $uri/ /admin.php$is_args$args; + } + + # PHP-FPM configuration + location ~ \.php$ { + fastcgi_pass unix:/var/run/php/php8.2-fpm.sock; + fastcgi_index index.php; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + } +} +``` + +## Organizing configuration + +### Configuration structure + +Create a configuration structure that separates concerns: + +``` +config/ +├── common/ # Shared between all entry points +│ ├── di/ # Dependency injection +│ │ ├── logger.php +│ │ ├── db.php +│ │ └── cache.php +│ ├── params.php # Common parameters +│ └── routes.php # Shared routes +├── web/ # Frontend-specific +│ ├── di/ +│ │ └── application.php +│ ├── params.php +│ └── routes.php +├── admin/ # Backend-specific +│ ├── di/ +│ │ └── application.php +│ ├── params.php +│ └── routes.php +├── console/ # Console commands +│ └── params.php +├── params.php # Root parameters +├── bootstrap.php # Application bootstrap +└── packages/ # Package configurations +``` + +### Configuring the config plugin + +Update `composer.json` to include admin configuration: + +```json +{ + "config-plugin": { + "common": "config/common/*.php", + "params": [ + "config/params.php", + "?config/params-local.php" + ], + "web": [ + "$common", + "config/web/*.php" + ], + "admin": [ + "$common", + "config/admin/*.php" + ], + "console": [ + "$common", + "config/console/*.php" + ], + "events": "config/events.php", + "events-web": [ + "$events", + "config/events-web.php" + ], + "events-admin": [ + "$events", + "config/events-admin.php" + ], + "providers": "config/providers.php", + "providers-web": [ + "$providers", + "config/providers-web.php" + ], + "providers-admin": [ + "$providers", + "config/providers-admin.php" + ], + "routes": "config/routes.php" + } +} +``` + +### Creating admin-specific configuration + +Create `config/admin/di/application.php`: + +```php + [ + '__construct()' => [ + 'dispatcher' => DynamicReference::to([ + 'class' => MiddlewareDispatcher::class, + 'withMiddlewares()' => [ + [ + ErrorCatcher::class, + SessionMiddleware::class, + CsrfTokenMiddleware::class, + Router::class, + ], + ], + ]), + ], + ], +]; +``` + +## Organizing source code + +### Directory structure + +Organize your source code to separate concerns while sharing common code: + +``` +src/ +├── Admin/ # Backend-specific code +│ ├── Controller/ +│ │ ├── DashboardController.php +│ │ └── UserController.php +│ ├── Service/ +│ │ └── UserManagementService.php +│ └── View/ +│ └── layout.php +├── Frontend/ # Frontend-specific code +│ ├── Controller/ +│ │ ├── SiteController.php +│ │ └── PostController.php +│ ├── Service/ +│ │ └── PostService.php +│ └── View/ +│ └── layout.php +├── Console/ # Console commands +│ └── Command/ +│ └── MigrateCommand.php +├── Shared/ # Shared code +│ ├── Entity/ +│ │ ├── User.php +│ │ └── Post.php +│ ├── Repository/ +│ │ ├── UserRepository.php +│ │ └── PostRepository.php +│ ├── Service/ +│ │ └── AuthService.php +│ └── ValueObject/ +└── Api/ # API-specific code (optional) + ├── Controller/ + └── Dto/ +``` + +### Example: Shared entity + +Create shared entities in `src/Shared/Entity/`: + +```php +id; + } + + public function getUsername(): string + { + return $this->username; + } + + public function getEmail(): string + { + return $this->email; + } + + public function getRole(): string + { + return $this->role; + } + + public function getPasswordHash(): string + { + return $this->passwordHash; + } + + public function isActive(): bool + { + return $this->status === 1; + } +} +``` + +### Example: Admin controller + +Create admin-specific controllers in `src/Admin/Controller/`: + +```php +userRepository->count(); + + return $this->responseFactory->createResponse([ + 'totalUsers' => $totalUsers, + ]); + } +} +``` + +### Example: Frontend controller + +Create frontend-specific controllers in `src/Frontend/Controller/`: + +```php +responseFactory->createResponse([ + 'title' => 'Welcome to Frontend', + ]); + } +} +``` + +## Setting up routes + +### Separate route configuration + +Create route configurations for each application section: + +#### Frontend routes + +Create `config/web/routes.php`: + +```php +action([SiteController::class, 'index']) + ->name('site/index'), + + Route::get('/post/{id:\d+}') + ->action([PostController::class, 'view']) + ->name('post/view'), + + Route::get('/posts') + ->action([PostController::class, 'index']) + ->name('post/index'), +]; +``` + +#### Backend routes + +Create `config/admin/routes.php`: + +```php +action([DashboardController::class, 'index']) + ->name('admin/dashboard'), + + Group::create('/users') + ->routes( + Route::get('/') + ->action([UserController::class, 'index']) + ->name('admin/user/index'), + + Route::get('/{id:\d+}') + ->action([UserController::class, 'view']) + ->name('admin/user/view'), + + Route::post('/{id:\d+}/edit') + ->action([UserController::class, 'update']) + ->name('admin/user/update'), + ), +]; +``` + +## Implementing access control + +### Creating authentication middleware + +Create admin-specific authentication middleware: + +```php +currentUser->getIdentity(); + + if (!$this->currentUser->isGuest() && $identity !== null && $identity->getRole() === 'admin') { + return $handler->handle($request); + } + + return $this->responseFactory + ->createResponse(302) + ->withHeader('Location', '/login'); + } +} +``` + +### Applying middleware to admin routes + +Update `config/admin/di/application.php` to include the authentication middleware: + +```php + [ + '__construct()' => [ + 'dispatcher' => DynamicReference::to([ + 'class' => MiddlewareDispatcher::class, + 'withMiddlewares()' => [ + [ + ErrorCatcher::class, + SessionMiddleware::class, + CsrfTokenMiddleware::class, + AdminAuthMiddleware::class, // Admin authentication + Router::class, + ], + ], + ]), + ], + ], +]; +``` + +## Managing shared resources + +### Shared database connections + +Configure database connections in `config/common/di/db.php` to be shared across all applications: + +```php + [ + 'class' => Connection::class, + '__construct()' => [ + 'driver' => new Driver( + $params['yiisoft/db-mysql']['dsn'], + $params['yiisoft/db-mysql']['username'], + $params['yiisoft/db-mysql']['password'], + ), + ], + ], +]; +``` + +### Shared services + +Create shared services in `src/Shared/Service/`: + +```php +userRepository->findByUsername($username); + + if ($user === null) { + return null; + } + + if (!password_verify($password, $user->getPasswordHash())) { + return null; + } + + return $user; + } +} +``` + +## Environment-specific configuration + +### Using environment variables + +Create `.env` files for different environments: + +#### Development environment + +Create `.env.dev`: + +```env +APP_ENV=dev +DB_HOST=localhost +DB_NAME=app_dev +DB_USER=dev_user +DB_PASSWORD=dev_password +``` + +#### Production environment + +Create `.env.prod`: + +```env +APP_ENV=prod +DB_HOST=production-db.example.com +DB_NAME=app_prod +DB_USER=prod_user +DB_PASSWORD=secure_password +``` + +### Loading environment variables + +Use [vlucas/phpdotenv](https://github.com/vlucas/phpdotenv) to load environment variables: + +```sh +composer require vlucas/phpdotenv +``` + +Update your entry scripts to load the appropriate environment file: + +```php +load(); + +// Define constants from environment +define('YII_ENV', $_ENV['APP_ENV'] ?? 'prod'); +define('YII_DEBUG', YII_ENV !== 'prod'); + +// Continue with application runner... +``` + +### Environment-specific parameters + +Create environment-specific parameter files: + +```php + [ + 'name' => $_ENV['APP_NAME'] ?? 'My Application', + 'charset' => 'UTF-8', + ], + + 'yiisoft/db-mysql' => [ + 'dsn' => sprintf( + 'mysql:host=%s;dbname=%s', + $_ENV['DB_HOST'] ?? 'localhost', + $_ENV['DB_NAME'] ?? 'app' + ), + 'username' => $_ENV['DB_USER'] ?? 'root', + 'password' => $_ENV['DB_PASSWORD'] ?? '', + ], +]; +``` + +## Handling assets and views + +### Separate view paths + +Configure separate view paths for frontend and backend in parameters: + +```php + [ + 'basePath' => '@root/resources/views/frontend', + ], +]; +``` + +```php + [ + 'basePath' => '@root/resources/views/admin', + ], +]; +``` + +### View directory structure + +Organize views by application: + +``` +resources/ +├── views/ +│ ├── frontend/ +│ │ ├── layout/ +│ │ │ ├── main.php +│ │ │ └── guest.php +│ │ ├── site/ +│ │ │ ├── index.php +│ │ │ └── about.php +│ │ └── post/ +│ │ ├── index.php +│ │ └── view.php +│ └── admin/ +│ ├── layout/ +│ │ └── main.php +│ ├── dashboard/ +│ │ └── index.php +│ └── user/ +│ ├── index.php +│ └── edit.php +└── assets/ + ├── frontend/ + │ ├── css/ + │ └── js/ + └── admin/ + ├── css/ + └── js/ +``` + +## API application + +### Creating an API entry point + +For RESTful APIs, create `public/api.php`: + +```php +run(); +``` + +### API configuration + +Create `config/api/di/application.php`: + +```php + [ + '__construct()' => [ + 'dispatcher' => DynamicReference::to([ + 'class' => MiddlewareDispatcher::class, + 'withMiddlewares()' => [ + [ + ErrorCatcher::class, + Router::class, + // Note: No CSRF for API, use token authentication instead + ], + ], + ]), + ], + ], +]; +``` + +### API routes + +Create RESTful routes in `config/api/routes.php`: + +```php +routes( + Group::create('/users') + ->routes( + Route::get('/') + ->action([UserController::class, 'index']) + ->name('api/user/index'), + + Route::get('/{id:\d+}') + ->action([UserController::class, 'view']) + ->name('api/user/view'), + + Route::post('/') + ->action([UserController::class, 'create']) + ->name('api/user/create'), + + Route::put('/{id:\d+}') + ->action([UserController::class, 'update']) + ->name('api/user/update'), + + Route::delete('/{id:\d+}') + ->action([UserController::class, 'delete']) + ->name('api/user/delete'), + ), + + Group::create('/posts') + ->routes( + Route::get('/') + ->action([PostController::class, 'index']) + ->name('api/post/index'), + + Route::get('/{id:\d+}') + ->action([PostController::class, 'view']) + ->name('api/post/view'), + ), + ), +]; +``` + +## Testing considerations + +### Separate test configurations + +Create test configurations for each application section: + +``` +tests/ +├── Admin/ +│ └── Controller/ +│ └── DashboardControllerTest.php +├── Frontend/ +│ └── Controller/ +│ └── SiteControllerTest.php +├── Api/ +│ └── Controller/ +│ └── UserControllerTest.php +└── Shared/ + ├── Entity/ + └── Service/ +``` + +### Example test + +```php +createMock(UserRepository::class); + $userRepository->method('count')->willReturn(42); + + $controller = new DashboardController( + new DataResponseFactory(), + $userRepository + ); + + $response = $controller->index( + $this->createMock(\Psr\Http\Message\ServerRequestInterface::class) + ); + + $this->assertSame(200, $response->getStatusCode()); + } +} +``` + +## Best practices + +1. **Keep shared code truly shared**: Only place code in `src/Shared/` if it's genuinely used by multiple applications +2. **Use middleware for separation**: Apply application-specific middleware instead of duplicating logic +3. **Leverage configuration**: Use the config plugin to manage environment and application-specific settings +4. **Follow PSR standards**: Ensure all code follows PSR interfaces for better interoperability +5. **Document entry points**: Clearly document each entry script and its purpose +6. **Use environment variables**: Externalize configuration that varies between environments +7. **Implement proper access control**: Use middleware and RBAC for securing admin areas +8. **Test each application separately**: Write tests that verify each application section works independently + +## Summary + +This structure provides clear separation between different application sections while maintaining a single codebase: + +- Multiple entry scripts with different configuration sets +- Organized source code structure separating frontend, backend, API, and shared code +- Configuration-based application setup +- Middleware-based access control and request handling + +The pattern allows you to maintain separate concerns for different parts of your application while sharing common code and resources efficiently. + +## References + +- [Application structure overview](../guide/structure/overview.md) +- [Configuration](../guide/concept/configuration.md) +- [Middleware](../guide/structure/middleware.md) +- [Routing](../guide/runtime/routing.md) +- [Structuring code by use-case with vertical slices](organizing-code/structuring-by-use-case-with-vertical-slices.md)