problem with CActiveRecord::$scenario

In my user registration controller, I’m doing something along the lines of this:




    $user = new User('signup');

    

    if(isset($_POST['User']))

    {

      $user->attributes = $_POST['User'];

      

      if ($user->save())

      {

        return $this->redirect('site/login',false);

      }

    }

    $this->render('index', array('user' => $user));



No problem with that, per se.

However, in User::beforeSave(), I have some conditional logic that depends on the current scenario.

Specifically, I have some code that should only execute when the scenario is NOT ‘signup’. In other words, my beforeSave() method contains something along the lines of this:




    if ($this->getScenario()!=='signup')

    {

      // some code that does not apply during sign-up...

    }



My problem is that this code ALWAYS executes. Why? Because of the way that the scenario is changed internally by CActiveRecord itself while it’s saving. CActiveRecord::populateRecord() and CActiveRecord::insert(), for example, both temporarily change the scenario to ‘update’.

That seems wrong to me - ‘update’ is not a scenario, it’s something else. Something that describes an internal condition to CActiveRecord.

I’m guessing the reason that it’s exposed this way, is so that you can write validation rules that apply only while active record is updating.

While that may be relevant, I may still need to know the actual scenario - the condition that I put my model into… you can’t just hide or erase the current scenario, it is very likely important to the developer - in my case, it’s preventing me entirely from doing what I need to do. I can’t think of any way to work around this, other than hacking CActiveRecord.

This to me indicates a design flaw.

It seems to me that the current internal operation/state of CActiveRecord can not be considered a “scenario” as such, since it does not describe the condition of my model. It’s more like a “state” or perhaps “operation”, and should not be mixed in with the scenario in which I’m using my model.

What’s worse is that there is no backwards compatible way to fix this issue, as far as I can figure… :frowning:

As it says in the documentation:

Scenario affects how validation is performed and which attributes can be massively assigned

For your need you can create a custom model attribute, give it a desired value when needed and in beforeSave check it’s value.

That’ll do as a work-around for now.

But storing the scenario in some other attribute to prevent it from being overwritten - that seems like a rather ugly work-around for something that would otherwise be very elegant and practical.

I insist that overwriting the scenario attribute is not a good approach - you’re throwing away potentially important information about the state of a model, and that can’t be a good thing.

It is also inconsistent with the behavior of CModel and CFormModel, where scenario persits for the duration of the request once you’ve set it.

The documentation for CActiveRecord::insert() states:


After the record is inserted to DB successfully, its isNewRecord property will be set false, and its scenario property will be set to be 'update'

The documentation for CActiveRecord::update() fails to mention that this method also overwrites the scenario.

Scenario is a general concept for models of any type. AR disregards this and uses it for a different purpose. The concept of creating or updating doesn’t exist in CModel, which is where the scenario concept is inherited from - so clearly this is not what it was designed or intended for.

In my opinion, the current state of AR (updating, inserting, etc.) can not be considered a scenario - it should have been a separate attribute with a different name, and this should have had separate support for filtering in validation rules.

This would also have enabled you to write more specific validation rules - for example, you could write a validation rule that applies not only in a given scenario, but under a given AR condition (updating, inserting, etc.).

It is quite likely that someone might need different validation rules during insert and update, for example a password field (required on insert, optional during updates) - the idea of setting the scenario attribute to ‘update’ accommodates that nicely.

The manual really doesn’t explain what precisely is meant by the concept of “scenario”, or what the attribute is for, beyond the fact that you can use it to filter validation rules. Wikipedia defines scenario as a narrative describing foreseeable interactions of types of users and the system". That’s how I understand it - I use it to describe a specific use case.

Usually, that use case is a lot more specific (and more important) than just knowing if we’re updating.

In fact, whether we’re updating or not is a simple boolean condition.

Overwriting a much more versatile attribute with a simple boolean flag, just seems wrong.

beforeSave is called before the the scenario is changed to update on insert so your code should work. Does your beforeSave method return true?

The code works the first time, but this particular model is saved twice during the same request - the second time, the scenario has been overwritten. As a work-around, I guess I can set the scenario again after saving.

Note that there are valid reasons for saving the same model twice during the same request. This particular model is part of an e-commerce solution, and the order processing and communication with the credit card processor happens in two stages. It is extremely important that the both responses from the credit card processor are saved - in case anything goes wrong during the second stage, this record is saved twice, to ensure that the first piece of information is secured, in case an error occurs during the second stage.

In some multi-tier applications, objects are passed back and forth between tiers. Since one tier can’t know if another tier is going to save the same object, sometimes the first tier has to make a change, then save the object - the second tier receives the object, makes another change, and saves the object again. Saving could involve some kind of history tracking, serial numbering, or tracking which tier generated what change.

I’m sure there are other valid reasons for saving the same object more than once.

Another case is that of related or contained models. I might have business logic for one model that depends on the scenario of another related model. If I can’t count on the scenario to persist during the request, implementing such relationships will require work-arounds as well.

I understand that in your case you want to keep the scenario unchanged. However, there are also use cases (perhaps many more) in which you need to insert a new record, make some changes, and then save (update) it. That’s the reason behind the current design.

So yes, you have to explicitly set the scenario again if you want to keep using "register" scenario for the same AR object after insertion occurs.

I understand why this feature is necessary.

What I don’t understand is why this feature has to interfere with the scenario feature itself.

I have same problem. And i fully agree with mindplay . Sceanrio - use for business logic of model, and used not only for DB operations like insert, update…

In my opinion this is very wrong that the framework is forced to change the scenario of using the model, set by me.

I agree - it’s a generic model feature being used for something that is internal to AR.

Maybe ‘on’=>‘update’ could be treated specially for backwards compatibility, but the state of AR is not a scenario and should be treated as something different - in validation rules, and in general.

I strongly agree with mindplay. I only figured out why $scenario was changing out from under me after hours of debugging, initially presuming that the unexpected and problematic behavior was in my code. At the very least this should be clearly documented, but with my systems architect hat on I would consider that a band-aid on the overloading of the $scenario property.

The functionality that is currently being addressed should indeed be addressed, but the two functions are semantically distinct and deserve their own variables. Personally, I think that it would be appropriate to get the "user action scenario" from the controller, not the model.

goes to research if the CController::action property is relevant for this

I’m not sure the current controller “scenario” (e.g. currently running action) should govern the use-case scenario of a model. At least not directly - indirectly, in a lot of cases, some statement within the action is probably going to control the Model scenario, but this could be based on more complex logic (for example, who the current user is) so you can’t just assume that the current action has anything to do with the scenario in which you’re using the model.

Conceivably, you could even have multiple instances of the same model, in the same action, being used in a different scenario - one could be influencing the other, so the scenarios could be “influencing” for the one, and “being influenced” for another. (speaking in abstract terms here - you wouldn’t actually use scenario names like that, because they’re not descriptive)

I think the thing that is misunderstood, is this: the model scenario describes the current circumstances under which a specific model is being used by your application.

And that doesn’t (or rather shouldn’t) have anything to with how Active Record (or some other framework component) may currently be using your model.

Those are two different pieces of information - that’s why I think placing AR state information in the scenario attribute is just wrong.

Come to think of it, here’s what I would recommend: Add another property named “condition” - and think of this not as meaning “circumstance”, but more like “what condition is it in” … think of it like the condition a car for sale is in, e.g. “new” or “used”, etc.

It occurs to me, that what AR is trying to keep track of, really isn’t a “scenario” - it doesn’t describe how the model is being used, but I think more accurately, it describes what condition the model is in. This could be enhanced to be more descriptive as well, to include conditions like “new”, “updated”, “unsaved”, etc.

As I may have pointed out earlier, this may well be useful or relevant to the application. For example, a validation rule may apply only to a model in a certain condition and in a specific scenario - say, the scenario "being edited by an administrator" in the condition "new". So validation rules probably need to be updated to support both.

This won’t be backwards compatible, I know - but it wasn’t right in the first place, so that’s not a good argument to keep things the way they are.

The change required to update an application to a new version of the framework would be minimal anyhow - basically, you can probably search and replace ‘on’=>‘update’ with ‘in’=>‘updated’… (or whatever the chosen property and value might be…)

If I understand you correctly, you want the "state" of an AR to be described more precisely in two dimensions: scenario and condition. The former describes the use-case scenario while the latter the state/condition of the object (new, updated, etc.)

IMO, the two dimensions can be merged into one dimension as what we have now: (ScenarioA, ConditionB) => "ScenarioA, ConditionB". For example, "being edited by an administrator" in the condition "new" can be phrased as a single senario as "being inserted by admin"; and "being edited by an administrator" in the condition "updated" becomes "being updated by admin".

It’s not quite a simple and understandable solution for me.

I stopped using AR’s scenario parameter after it got changed by AR’s internals.

For you as the designer, the concept of scenarios may be different from a newbie like me would understand.

When we say ‘scenarios’ i think of ‘parametrizing validation rules’ which are business logic. That was said before in this topic.

AR’s internals and “my” business logic are for me two different things.

My concept is to use validation and scenarios to set the state of the record and make it persistent. I need to sometimes do some actions defined in behaviors based on the state and current scenario.

I’m not saying that the current concept is good or bad.

I’m just saying that for me is difficult to understand why currently does this work this way. I would be happy to have full control over ‘scenario’ attribute.

Best regards.

But how would AR "merge" an existing scenario, defined by my application, with a condition defined by AR?

And even if it could do that, say, by appending it’s condition to the scenario string, how would you work that into your validators for rules that apply to a given scenario in any condition? “being edited by admin, being inserted by admin, being delete by admin” etc.

You could merge them, sure, but it wouldn’t be practical at all.

Reading this again, allow me to clarify - no, I don’t want the state of AR to be described in two dimensions, AR shouldn’t touch the scenario setting at all, it should have it’s own property for that, e.g. condition.

I think the reason why the use-case descriptor, scenario, is built into the CModel class in the first place, and not into CActiveRecord, is because it’s a general-purpose property that is applicable to any kind of model, not just persistent models.

That’s another reason why CActiveRecord actually interferes with the normal use of the scenario property by using it to describe a condition that is specific to persistent models - it changes the meaning of the scenario property from “what is my application doing with this model”, to “what is AR doing with this model”.

I know it’s impractical to change it at this point, but I have to insist that these are two different dimensions.

Nope, you do the merging (conceptually, by giving a meaningful scenario name that has the condition name embedded), not AR.

Let’s say you have an AR in ‘new’ condition (IsNewRecord=true), and you want to collect input and do validation. You would do

  • Your proposal: $model->scenario=‘being edited by an administrator’

  • Current solutioin: $model->scenario=‘being inserted by an administrator’

And if the AR is in ‘updated’ condition (IsNewRecord=false), you would do

  • Your proposal: same as above

  • Current solution: $model->scenario=‘being updated by an administrator’

As the developer, you should know the state of an AR object when setting the scenario. Yes, your proposed solution allows you to forget about the AR state and purely rely on the scenario to do the magic. I’m not sure if this is common in practice. However, if this is desired, with the current solution, you can still have the workaround like: $model->scenario=‘being ‘.($model->isNewRecord ? ‘inserted’:‘updated’).’ by an administrator’;

And my biggest concern with your proposed solution is that it may confuse people by introducing a new concept (not really new, but forcing people to understand some very subtle concepts) while it is probably mainly useful in some corner cases.

Please correct me if I understand you wrong.

@veris: AR only sets the scenario automatically after you call insert() or if an AR instance is created by some find* method. In both cases, the scenario is set to be ‘update’. This automatic setting usually won’t stand in your way unless you are doing the following:




$model=new Post($scenario);

...

$model->save();  // after this line, the scenario becomes 'update'

...

$model->save();  // you call save() again on the SAME model (not common)



You can change the scenario after the first save() if you think the automatic setting is bad:




$model->scenario=$scenario;



As defined in CModel:

Scenario affects how validation is performed and which attributes can be massively assigned.

So as I understand, we assume it’s used only for that and after the validation we don’t need ‘scenario’ in CModel anymore.

So it’s reasonable to use it for AR’s statemachine management.

We also assume that validation is performed once and it’s final.

Basically it seems that that’s all.

Second thing is understanding how the statemachine works and how we check its state by reading the scenario attribute.

So should anything that doesn’t fit into attribute validation or statemachine management be handled separately and by not using ‘scenario’?

ps. I edited this post at least 15 times. It shows that reading every word in documentation is important and knowing internals of the design too.

Correct me if i’m wrong. Thank you for your effort and time.

I don’t want to edit my post again so I write another reply.

Maybe the documentation needs to be changed to better explain what is the scenario used for and what not. And what is expected usage and meaning of the scenario during the lifetime of an object.

Because the factor is also the class the functionality belongs to.

Some functionality is performed in CModel (parent) and some in CActiveRecord (child) and this seem to define the usage and change in meaning of the attribute.

What do you think about that?