In Michael Hartl's rails tutorial we have a current_user method defined as such:
# Returns the user corresponding to the remember token cookie.
def current_user
if (user_id = session[:user_id])
#current_user ||= User.find_by(id: user_id)
elsif (user_id = cookies.signed[:user_id])
user = User.find_by(id: user_id)
if user && user.authenticated?(:remember, cookies[:remember_token])
log_in user
#current_user = user
end
end
end
At first I assumed #current_user was needed as opposed to some other local variable like the_current_user (assuming you can't use current_user since that's the name of the method.
When using helper methods that return something, do we need an # variable or can we just use any variable type? (I saw other helper_methods use normal variable_names). (Im assuming #current_user was just convenient)
# makes variables available throughout the class.
If was just current_user instead of #current_user it would only be accessible inside of that specific method.
You can use current_user as a variable name inside of a method named current_user
you don't need to use # before an instance variable to return a value.
def some_method
user = "Jimmy"
user
end
> puts some_method
"Jimmy"
Here's a common Ruby idiom, up close:
def current_user
#current_user ||= User.find_by(id: something)
end
You can call current_user as often as you like, now, because it will only spend time hitting the database the first time you call it. On subsequent calls, #current_user has a value, so the ||= evaluates trivially as #current_user = #current_user.
The effect lasts as long as the # instance exists. If it's a controller, it will last for the current action, and then disappear. This means a new action with a different user will not trip over the previous value of #current_user.
Because def current_user occupies a namespace different from #current_user, the Ruby idiom is to name the "memento pattern" variable the same as the method it optimizes.
Your example makes the memento pattern a little confusing, because there are two ways to generate a current user.
You can return normal variable in helper methods.
# is just a syntax used to define an instance variable.
#current_user is just a convention, you can use any name like #whatever_user
For example, if we are using current_user instead of #current_user then you will not be able to access current_user from any of your views.
For example, if you want to show the name of the user if the user is signed in:
<%= #current_user.name if #current_user %>
It is possible only because we have used the instance variable.
Following will help you in knowing more about instance variable:
https://ruby-doc.org/docs/ruby-doc-bundle/UsersGuide/rg/instancevars.html
http://ruby-for-beginners.rubymonstas.org/writing_classes/instance_variables.html
To know about all the types of variables available in Ruby:
https://www.studytonight.com/ruby/types-of-variables-in-ruby
Side Note
#current_user ||= User.find_by(id: user_id)
This pattern is called memoization. It is a very common pattern in Ruby/Rails. You can read more about it here:
https://www.justinweiss.com/articles/4-simple-memoization-patterns-in-ruby-and-one-gem/
Related
I'm a newbie working my way through Hartl's Rails 5 tutorial when I came across this piece of confusing code listed below. It looks like #current_user is being defined within a method as an instance variable, so that it may be used outside the method. However, in the logged_in? method, no # symbol is needed to call on it. In fact, if I add rewrite logged_in? to use #current_user, it no longer works as intended!
module SessionsHelper
# Logs in the given user.
def log_in(user)
session[:user_id] = user.id
end
# Returns the current logged-in user (if any).
def current_user
if session[:user_id]
#current_user ||= User.find_by(id: session[:user_id])
end
end
# Returns true if the user is logged in, false otherwise.
def logged_in?
!current_user.nil?
end
end
When I try this in ruby, I'm unable to use an instance variable in this way. The example I tried is below. When I call method2, I get an error that isn't fixed until I add an # symbol in front of var1. Can anyone help me understand this? Thanks!!
def method1
#var1 = 2
#var1
end
def method2
#var1 == 2
end
puts method1
puts method2
current_user is being defined as a method, and then inside the method, an instance variable #current_user is defined, which is only used inside the method. The logged_in? method is calling the current_user method, it is not accessing the #current_user variable directly. Note that in ruby, the #, $, or ## are actually part of the variable name, and are not modifiers. Therefore, current_user and #current_user are not the same entity.
module SessionsHelper
# Logs in the given user.
def log_in(user)
session[:user_id] = user.id
end
# Returns the current logged-in user (if any).
def current_user
#current_user ||= User.find_by(id: session[:user_id])
end
# Returns true if the user is logged in, false otherwise.
def logged_in?
!current_user.nil?
end
end
Currently working through Hartl's Rails Tutorial in Ch 8 where he gets you to write code for users to log in and stay logged in.
Under method logged_in?, why is local variable current_user used instead of #current_user?
current_user is not a local variable, it's an instance method.
why is instance variable not used?
It is being used.
When you call current_user method it returns an instance variable #current_user, which happens to be either a User object or nil.
That isn't a local variable! He's calling the current_user method which returns the #current_user value and so chains nil off of it. You need to look into scope in ruby to see how methods and instance variables and local variables interact with one another!
Because current_user - is method defined in same module. This technique called memoization. You can read about here http://www.justinweiss.com/articles/4-simple-memoization-patterns-in-ruby-and-one-gem/.
I am in section 8.4.3 - Forgetting Users of the Rails Tutorial.
# app/helpers/session_helper.rb
module SessionsHelper
...
# Returns the user corresponding to the remember token cookie.
def current_user
if (user_id = session[:user_id])
#current_user ||= User.find_by(id: user_id)
elsif (user_id = cookies.signed[:user_id])
user = User.find_by(id: user_id)
if user && user.authenticated?(cookies[:remember_token])
log_in user
#current_user = user
end
end
end
# Logs out the current user
def log_out
forget(current_user)
session.delete(:user_id)
#current_user = nil
end
...
end
In the current_user method we assign a user to the #current_user instance variable. I don't understand why we don't use that same instance variable in the log_out method (in that method current_user is not prepended with an # symbol). Where does current_user come from then since it isn't passed in as an argument to that method?
Ruby doesn't make difference between a method invocation and a local variable. That is the reason why current_user might look like both.
In your case it's the defined method.
You can actually use both #current_user and current_user as the returned value of that method should be equal to #current_user.
current_user is the method defined at the snippet you posted of the page.
#current_user is the instance variable set BY current_user
So when you look at the code:
# Defines a method called log_out
def log_out
# Calls the 'forget' method on the result of the 'current_user' method,
# which sets #current_user
forget(current_user)
# Deletes the session variable named 'user_id'
session.delete(:user_id)
# Resets #current_user to nil, because there should no longer be one
#current_user = nil
end
If I wanted to look at it differently, I could say:
def log_out
# Sets #current_user
current_user
# Calls the forget method on the variable #current_user
forget(#current_user)
session.delete(:user_id)
#current_user = nil
end
But I could NOT change the last line to current_user = nil instead of #current_user = nil, because then I am assigning the nil value to the name of a method instead of to the instance variable.
Part of the reason method calls and variables look so similar in Ruby is that everything is handled as an object, so methods are essentially just a variable whose value is a block of code (there are technical differences to what we call a "block" and a method, but conceptually this is a decent analogy). When we define a method, we save it into what is, more or less, a variable. Thus if we think about the method "current_user", we may instead think of it as a variable "current_user" whose value is forget(current_user); session.delete(:user_id); #current_user=nil.
This is an oversimplification, of course, but it is worth keeping in mind as you go forward with your learning.
I don't understand why current_user is nil after deleting user_id from session. When the current_user function is called wouldn't it return the old current_user value again?
#Returns the current logged-in user (if any).
def current_user
#current_user ||= User.find_by(id: session[:user_id])
end
# Logs out the current user.
def log_out
session.delete(:user_id)
# #current_user = nil
end
I assume these methods are defined in a controller. Controller instance variables only have a lifespan of a single HTTP request. Session values last longer, but you've deleted the session value that matters here.
When the next request comes in, #current_user needs to be set again, and since session[:user_id] is nil, you are essentially calling User.find_by(id: nil), which will return nil.
So I'm following the Rails Tutorial, and I've gotten to the portion where we want to sign a user in with a sign_in SessionHelper.
Question 1:
module SessionsHelper
def sign_in(user)
cookies.permanent.signed[:remember_token] = [user.id, user.salt]
current_user = user
end
def current_user=(user) #set current_user
#current_user = user
end
def current_user #get current_user
#current_user
end
What I'm having difficulty with is the part that reads:
The problem is that it utterly fails to solve our problem: with the code the user's signin status would be forgotten: as soon as the user went to another page.
I don't understand how this is true? I read on and understand the added code makes sure #current_user is never nil. But I'm not seeing how current_user would revert to nil if we just established it in 5th line.
Question 2:
The updated code reads as such:
module SessionsHelper
def sign_in(user) #in helper because used in view & controller
cookies.permanent.signed[:remember_token] = [user.id, user.salt]
current_user = user
end
def current_user=(user) #set current_user
#current_user = user
end
def current_user #get current_user
#current_user ||= user_from_remember_token #<-- short-circuit evaluation
end
private
def user_from_remember_token
User.authenticate_with_salt(*remember_token) #*=use [] instead of 2 vars
end
def remember_token
cookies.signed[:remember_token] || [nil, nil]
end
end
In the remember_token helper, why does it use cookies.signed[] instead of cookies.permanent.signed[] & why doesn't it use ||= operator we just learned about?
Question 3:
Why do we need to authenticate_with_salt? If I authenticate & sign_in can see the id & salt attributes from the user who was passed to it, why do we need to double_check it? What kind of situation would trigger a mixup?
Remember that instance variables like #current_user are only set for the duration of the request. The controller and view handler instances are created specifically for rendering once and once only.
It is often easy to presume that because you've set a variable somewhere that it will continue to work at some point in the future, but this is not the case. To preserve something between requests you need to store it somewhere, and the most convenient place is the session facility.
What's missing in this example is something along the lines of:
def current_user
#current_user ||= User.find_by_remember_token(cookies[:remember_token])
end
Generally it's a good idea to use the write accessor to map out the functionality of the sign_in method you've given as an example:
def current_user=(user)
cookies.permanent.signed[:remember_token] = [user.id, user.salt]
#current_user = user
end
It's odd that there is a specific "sign in" method when the act of assigning the current user should be the same thing by implication.
From a matter of style, though, it might be more meaningful to call these methods session_user as opposed to current_user for those situations when one user is viewing another. "Current" can mean "user I am currently viewing" or "user I am currently logged in as" depending on your perspective, which causes confusion. "Session" is more specific.
Update:
In response to your addendum, the reason for using cookies to read and cookies.permanent to assign is much the same as using flash.now to assign, and flash to read. The .permanent and .now parts are intended to be used when exercising the assignment operator.