I need suggestions and guidance on how to apply the SOLID principles within a monolithic structure

I have been working on a Yii2 project using the advanced template for the past three years. Unfortunately, it has turned into a large, messy monolith without a proper object-oriented programming (OOP) structure, and it does not adhere to the SOLID principles. Additionally, test cases have been missing up to this point.

Despite this, the project continues to grow daily, with new features and products introduced every quarter.

I’m refactoring my code from the beginning and writing test cases. What design pattern or code repository structure should I use to ensure stability, testing, and avoid technical debt?

Please share your suggestions regarding the solution I researched today.

config/web.php

'container' => [
    'definitions' => [
        'app\services\AuthService' => [
            'class' => 'app\services\AuthService',
            'constructorArgs' => [
                'userRepository' => function () {
                    return new app\repositories\UserRepository();
                },
                'authUserSettingRepository' => function () {
                    return new app\repositories\AuthUserSettingRepository();
                },
            ],
        ],
        'app\services\TwoFactorAuthService' => [
            'class' => 'app\services\TwoFactorAuthService',
            'constructorArgs' => [
                'userRepository' => function () {
                    return new app\repositories\UserRepository();
                },
                'otpRepository' => function () {
                    return new app\repositories\OtpRepository();
                },
                'emailSender' => function () {
                    return new app\components\EmailSender();
                },
            ],
        ],
    ],
],

repositories/AuthUserSettingRepository.php

<?php

namespace app\repositories;

use app\models\AuthUserSetting;

class AuthUserSettingRepository
{
    /**
     * Find auth settings by user ID
     *
     * @param int $userId
     * @return AuthUserSetting|null
     */
    public function findByUserId($userId)
    {
        return AuthUserSetting::findOne(['user_id' => $userId]);
    }
}

repositories/UserRepository.php

<?php

namespace app\repositories;

use app\models\User;

class UserRepository
{
    /**
     * Find a user by email
     *
     * @param string $email
     * @return User|null
     */
    public function findByEmail($email)
    {
        return User::findOne(['email' => $email]);
    }
}

controllers/AuthController

<?php

namespace app\controllers;

use Yii;
use yii\web\Controller;
use app\models\LoginForm;
use app\services\AuthService;
use app\services\TwoFactorAuthService;
use yii\filters\AccessControl;
use yii\filters\VerbFilter;

class AuthController extends Controller
{
    private $authService;
    private $twoFactorAuthService;

    public function __construct(
        $id,
        $module,
        AuthService $authService,
        TwoFactorAuthService $twoFactorAuthService,
        $config = []
    ) {
        $this->authService = $authService;
        $this->twoFactorAuthService = $twoFactorAuthService;
        parent::__construct($id, $module, $config);
    }


    public function actionLogin()
    {
        if (!Yii::$app->user->isGuest) {
            return $this->goHome();
        }

        $model = new LoginForm();

        if (Yii::$app->request->isPost && $model->load(Yii::$app->request->post()) && $model->validate()) {
            $loginResponse = $this->authService->authenticate($model->email, $model->password);

            if ($loginResponse->isSuccess()) {
                if ($loginResponse->requiresTwoFactor()) {
                    // Store email in session for the OTP verification
                    Yii::$app->session->set('email_for_otp', $model->email);

                    // Send OTP
                    $this->twoFactorAuthService->sendOtp($model->email);

                    // Redirect to OTP verification page
                    return $this->redirect(['auth/verify-otp']);
                }

                // Regular login successful
                return $this->goBack();
            } else {
                // Set flash message
                Yii::$app->session->setFlash('error', $loginResponse->getMessage());
            }
        }

        return $this->render('login', [
            'model' => $model,
        ]);
    }

Thanks

  1. Check “vertical slices”
  2. Read the guide on modules, it may help

You can’t avoid technical debt by just adhering to a structure or applying some design patterns.

2 Likes

Thank you @samdark for suggestion I will check out

@samdark
After few research on the vertical slices I really like the architecture patteren and this is really what I want. Thanks a a lot.

Below is the directory structure for the auth service in yii2 with MVC and vertical slice

frontend/
  /features/
    /auth/
      /login/
        - LoginForm.php
        - LoginHandler.php
        - LoginAction.php
        - login.php (view)
        - LoginTest.php
      /signup/
        - SignupForm.php
        - SignupHandler.php
        - SignupAction.php
        - signup.php (view)
        - SignupTest.php
      /request-password-reset/
        - PasswordResetRequestForm.php
        - PasswordResetRequestHandler.php
        - PasswordResetRequestAction.php
        - request-password-reset.php (view)
        - PasswordResetRequestTest.php
      /reset-password/
        - ResetPasswordForm.php
        - ResetPasswordHandler.php
        - ResetPasswordAction.php
        - reset-password.php (view)
        - ResetPasswordTest.php
      /logout/
        - LogoutAction.php
        - LogoutTest.php

Now in the controller I will define these actions

    /**
     * {@inheritdoc}
     */
    public function actions()
    {
        return [
            'login' => LoginAction::class,
            'signup' => SignupAction::class,
            'request-password-reset' => PasswordResetRequestAction::class,
            'reset-password' => ResetPasswordAction::class,
            'logout' => LogoutAction::class,
        ];
    }

Am I going in the right direction? Also, is this architecture feasible? I mean, does it work in production or in the long run, and is it scalable? I think yes

Please guide me. I am also planning to conduct unit testing for each feature.

Yes, direction is right, and this layout scales well until you need more separation. You can do it like that (similar to Yii3 approach) or you can leverage Yii2’s modules. These are quite similar.

Having tests is an excellent idea.

Thank you! I will start refactoring my project to this architecture. And once Yii3 is fully released, I also look forward to migrating from Yii2 to Yii3.