1

I have used the state pattern for my web application where a user can be in one of three states: activated, required_activation and suspended. I have created 3 states classes as given below (source is in PHP) and a variable named state holds the instance of a state out of 3.

class User {
    private UserState $state;
    public function __construct()
    {
        $this->state = new ActivationRequiredUserState()
    }
}


abstract class UserState {
    public abstract function login(User $user): void;
}


class ActivatedUserState extends UserState {
    public function login(User $user): void {
        // implement login and return token.
    }
}

class SuspendedUserState extends UserState {
    public function login(User $user): void {
        // throw an exception with message that suspended user can't login.
    }
}

class ActivationRequiredUserState extends UserState {
    public function login(User $user): void {
        // throw an exception with message that please activate your account before login.
    }
}

class UserStateContext {
    $user = new User();

    $user->state->login($this);
}

So far, so good, I have no issue and it was working fine.

The Actual Issue

A user belongs to a company and has its own states like activated, required_activation and suspended. When I call $user->state->login() I now have to consider the state of the Company object as well. As it is obvious that if a company is in suspended state then it doesn't matter that whatever state a user in, I have to stop logging in action and throw an error but with an error that the company is suspended NOT DELETED or NOT ARCHIVED.

For this I have a solution that out of three UserStates I put a check (if condition) in the ActivatedUserState which will check if the company is not in suspended state as given below:

class ActivatedUserState extends UserState {
    public function login(User $user): void {
        if($company->status !== 'suspended') {
            // implement login and return token.
        } else {
            // throw an error with the message the company is suspended.
        }
    }
}

But this then introduces the if/else and will make it more and more complex if my company object has few more states.

NOTE: This question is very much related to this one which has the following two parts:

  1. Should I use the State Design pattern for only two states?
  2. Also, what if one object's state is affected by another state?

According to the question asker part 1 of the question is answered which easy for me and I already have implemented more than 2 states for my User and Company objects. But part 2 of his question, which similar to my question is not answered as Sean had mentioned in the comments of the answer (which is incomplete answer).

5
  • What is the relationship between User and Company? Can a User be part of zero or more Companies?
    – Erik Eidt
    Dec 5 '21 at 20:12
  • currently the user can be the part of only one company not more than one or zero
    – MKJ
    Dec 6 '21 at 3:25
  • You shouldn't ask what the state of the company is - that's internal to the company object, it's an internal representation that should be invisible to you ("you" meaning the calling code). You should ask the company object to attempt a login, let it internally process the request & determine how to respond based on its state, before returning control back to the caller. The caller can then decide how to deal with the response. Remember - write things in terms of communicating encapsulated objects. Dec 6 '21 at 12:30
  • If you mean to this: $company->state->login(); along with $user->state->login(); then it doesn't make sense to me. Because a user can login but a company cannot login. But the state of the company along with the state of the user decides if the user should login or not. Can you please explain with code?
    – MKJ
    Dec 7 '21 at 2:18
  • No, there's no interaction/communication there. Depends on what else you're doing, but one way would be $company->attemptLogin($credentials, 'callbackOnSuccess', callbackOnFailure'), where the success callback could accept the token and do something with it, and the failure callback could accept an object (or just a string) describing why login failed. Or, instead of a callback, you could pass a full-blown object to call methods on. Or if that doesn't quite work, adjust the design, maybe introduce an intermediate object that you can call, then reveal state to it, but not to the callers. Dec 7 '21 at 7:28
0

Neither the User nor the XXXUserState classes have any business knowing what states a Company can have nor how those states are implemented.

If there is a requirement that is a Company is suspended that then all users belonging to that company must be blocked from logging in, then the Company class should expose a method canUserLogin() that returns if logins for that company are allowed or not. Calling that function adds a single if statement in the login logic for a user, but that won't expand when new states come up for a Company.

It depends on the requirements (which error is more important to give if the company is suspended and the user not yet activated) where the check can best be placed if the company state allows a login. That could be either within the ActivatedUserState as you did already, or within the User class before delegating the login process to the current state object.

2
  • Thanks for the answer. But if I use this approach then I will have make conditions it will be get complex if I have one more object say billing, with two states paid and unpaid. Then I will have to check like this: if ($company->canUserLogin() && $billing->canUserLogin()) { $user->state->login();} Then don't I am back to place I had started: "Remove conditions with Polymorphism".?
    – MKJ
    Dec 7 '21 at 2:31
  • @MKJ, another approach is that the login blockage caused by a wrong state of billing (or the company) causes the User to go into a new state where login gets blocked. Then you transfer the if conditions into a more complex statemachine. Dec 7 '21 at 7:03

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.