How context should be used in phoenix 1.3?

Hi,
I have trouble understand how context should be used.
Let say that I run :
> mix phx.gen.html Accounts User users email:string:unique password_hash:string is_admin:boolean

This will create the User structure under the Accounts context, which means that it will create 2 files, accounts.ex and user.ex.
My question is what kind of function should be put respectively in account.ex, and in user.ex ?

Let say for exemple that I want to hash the password while registration. With the previous versions of phoenix I would have done it in user.ex more or less like that, with hash_password a private function who do what it says :
> def registration_changeset(struct, params) do
struct
|> changeset(params)
|> cast(params, ~w(password)a, [])
|> hash_password
end

Instinctively I would have still put it in user.ex because hashing the password should be the only concern of the user. But I thought that I may be wrong because the generator define user_changeset, change_user in account.ex

Thx

3 Likes

From my understanding, in the accounts.ex module (your context) you should write the functions to manipulate your users (create_user, register_user, list_usersā€¦) while in the user.ex you would solely describe your schema.

The context module is just hiding the details of your operation from other modules (like your controllers) so they donā€™t have to deal with the Repo module (or any other mechanism you are working with).

However, I do agree with you about the changeset functions. Iā€™d write them in the user.ex module instead of accounts.ex. Iā€™ve done it myself in my experiments. Maybe someone else could explain if this is ok?

But, as many others have said, you do what you find is the best approach for your needs :slight_smile:

5 Likes

I strongly think we should move the changesets back to schema modules.

  • Validations and checking for data integrity belongs to the schema.
  • Data manipulation, such as hash_password, store_last_4_digits_of_bank_account, etc, should belongs to the schema too, or create a service module for them.

I think the context file only acts as an interface for the context, keep it lean, similar to how we should keep the controller thin/lean.

8 Likes

I have been working with the rc0 for nearly a week now and I moved back all changeset functions back to the schema instead of leaving it in the context.
Itā€™s much cleaner and sensible in my situation where I have many schemas/models within a context.

I think eventually I have to break out the main context into few smaller contexts.

4 Likes

Iā€™ve been working with Phoenix 1.3.rc0 too for the last couple of days. I am of the same opinion that keeping the ā€œchangesetā€ functions to the schemas/models is the preferred approach.

As I see it and have understood it, the context should be used as an internal API for communication between different contexts. Here comes the tricky part of defining clear and sane contexts.

4 Likes

Then am i right to say that every module in a context (but the context module) should contains internal ā€˜privateā€™ function of the context ?

2 Likes

To be honest, Iā€™m not sure I understand the question.

I see it like that. Letā€™s imagine that you have two contexts - Accounts.{User, Address, etc} & Billing.{Invoice, NotificationsService, etc}.

If you wish to retrieve a User from within the context of a Billing.Invoice, you are supposed to do that through the Accounts context. E.g. Accounts.get_user_by_email/1. I think it is alright to use the %Accounts.User{} struct, yet you shouldnā€™t access itā€™s public functions/methods. However, you can use the public functions of modules that are part of the same context. E.g. You can call Billing.NotificationService.deliver_email/1 from within Billing.Invoice :). At the same time, you should call something like Billing.notify_client/1, which will dispatch to Billing.NotificationService.some_public_function/1, from any Module thatā€™s not part of the Billing context :dizzy_face:

Of course, donā€™t take my advice/words for granted, Iā€™m a newbie myself :smiley:

1 Like

I think @rms.mrcs sums it up pretty well but hereā€™s some additional context (ha) why you may want have changeset functions in the context module:

We generate changesets in the context for a couple reasons. First, we want to place the changeset building closer to the user input, so folks start thinking about decoupling their db schemas from the various shapes of user input that they need to cast and validate. Another reason is the schemas we generate are sharable across boundaries, so we donā€™t want folks to see the changeset functions there and start violating boundaries between parts of their system.

3 Likes

And to someone not following that issue thread, seems like current master branch of phoenix now puts the changesets in the schema files instead of the context:

josevalim: We changed because we thought it would be a better example of how to break your code apart. Mostly bullet 2 above

That was in answer to this question:

As with any code, if you feel your code is becoming large, coupled, and ugly, then itā€™s time to refactor and think about how you can cleanly isolate and clean up the moving parts :slight_smile:

But how?

  • Breaking it up into different contexts?
  • Moving stuff into the schema (changesets, validation logic, etcā€¦)
  • Any of the above or something else?

I have two schemas in my context and itā€™s over 380 lines with one more schema to come.

2 Likes

What about file uploads?

Should the file persistence happen in the context because it is part of the service logic?

Should it happen in the schema because a file is a part of the data?

Should it happen in the controller because the file should be put in a directory that is accessible to the internet?

1 Like