If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
If the current user has a Facebook ID, let's replace the "Connect with Facebook" link with one called "Share" that will post to their timeline:
{# views/dashboard.twig #}
<div class="panel-body">
{% if user.facebookUserId %}
Share how many eggs you've collected today on Facebook!
<a href="{{ path('facebook_share_place') }}" class="btn btn-info">Share</a>
{% else %}
Share your status on Facebook!
<a href="{{ path('facebook_authorize_start') }}">Connect with Facebook</a>
{% endif %}
</div>
The URL I'm generating here is pointing to a function called shareProgressOnFacebook
in FacebookOAuthController:
// src/OAuth2Demo/Client/Controllers/FacebookOAuthController.php
// ...
public function shareProgressOnFacebook()
{
die('Todo: Use Facebook\'s API to post to someone\'s feed');
return $this->redirect($this->generateUrl('home'));
}
Click the link to see the message in my die
statement being printed.
To post to someone's timeline, we'll use Facebook's API. Like with any API that uses OAuth, we just need to know the URL, the HTTP method, any data we need to send, and how the access token should be attached to the request.
With some quick googling, we see that we need to make a POST request to
/[USER_ID]/feed
and send message
and access_token
POST data.
We could absolutely do this manually, using the nice Guzzle library from before. But since we're using the Facebook SDK, it's even easier.
Use the createFacebook
method from before to get our Facebook object
and then use its api
method. This takes 3 arguments: the API URL, the
HTTP method, and any parameters we need to send:
public function shareProgressOnFacebook()
{
$facebook = $this->createFacebook();
$facebook->api(
'/'.$facebook->getUser().'/feed',
'POST',
array(
'message' => 'TEST',
)
);
die('Todo: Use Facebook\'s API to post to someone\'s feed');
// ...
}
The handy $facebook->getUser()
method gives us the right USER_ID
for
the URL. The only missing piece is the access_token
parameter, which we
can leave out because the Facebook class adds that automatically for us. Again,
that's really cool - just don't lose sight of how things are really working
behind the scenes.
Let's set the return value to a variable and dump it:
$result = $facebook->api(
'/'.$facebook->getUser().'/feed',
'POST',
array(
'message' => 'TEST',
)
);
var_dump($result);die;
Refresh the page to try it out. It prints out an array with an id
and
a long number string. The response from api
is specific to what you're
trying to do. In this case, this is the ID of the new post it made. When
I go to my Facebook page, there's my egg-citing post!
Remember that one of the reasons this works is that our authorization URL
included the scope publish_actions
. Had we not done that, this request
would fail.
Tip
With Facebook and other OAuth servers, users are able to approve some of the scopes requested by your application but deny others. So code defensively - API requests may fail!
Let's make the message more realistic by putting in my egg count and finish the flow by redirecting back to the homepage:
public function shareProgressOnFacebook()
{
$facebook = $this->createFacebook();
$eggCount = $this->getTodaysEggCountForUser($this->getLoggedInUser());
$facebook->api(
'/'.$facebook->getUser().'/feed',
'POST',
array(
'message' => sprintf('Woh my chickens have laid %s eggs today!', $eggCount),
)
);
return $this->redirect($this->generateUrl('home'));
}
Refresh to try it all again. Check Facebook to see that we're bragging about our egg-laying hens' progress!
Of course, the API request may fail, especially in the world of OAuth where
the access token might be expired. If any API request fails, the Facebook
class will throw a FacebookApiException
. That's great, because
we can wrap the API call in a try-catch block:
try {
$facebook->api(
'/'.$facebook->getUser().'/feed',
'POST',
array(
'message' => sprintf('Woh my chickens have laid %s eggs today!', $eggCount),
)
);
} catch (\FacebookApiException $e) {
// it failed!
}
If you want to get information about the error, the exception object has
a few useful methods, like getResult()
, which gives you the raw API error
response or getType()
and getCode()
. Facebook has a helpful page called
Using the Graph API that talks about the API and also the errors you might
get back. If getType()
returns OAuthException
, or if the code is
190 or 102, the error is probably related to OAuth and we should try
re-authorizing them:
try {
$facebook->api(
'/'.$facebook->getUser().'/feed',
'POST',
array(
'message' => sprintf('Woh my chickens have laid %s eggs today!', $eggCount),
)
);
} catch (\FacebookApiException $e) {
// https://developers.facebook.com/docs/graph-api/using-graph-api/#errors
if ($e->getType() == 'OAuthException' || in_array($e->getCode(), array(190, 102))) {
// our token is bad - reauthorize to get a new token
return $this->redirect($this->generateUrl('facebook_authorize_start'));
}
// it failed for some odd reason...
throw $e;
}
There's even another page that talks about handling expired tokens in more detail. If this seems a little unclear, that's probably because Facebook's error documentation is a little fuzzy.
If it's any other error, I'll just throw the original exception. You could even render some custom error page.
With any API that uses OAuth, if you can be smart enough to detect when API requests fail due to an expired access token, you can give your users a better experience by having them re-authorize your application instead of just failing.
Depending on the error, you might also want to re-try the request. Let's
refactor the API call into a new private method called makeApiRequest()
:
public function shareProgressOnFacebook()
{
$eggCount = $this->getTodaysEggCountForUser($this->getLoggedInUser());
$facebook = $this->createFacebook();
$ret = $this->makeApiRequest(
$facebook,
'/'.$facebook->getUser().'/feed',
'POST',
array(
'message' => sprintf('Woh my chickens have laid %s eggs today!', $eggCount),
)
);
// if makeApiRequest returns a redirect, do it! The user needs to re-authorize
if ($ret instanceof RedirectResponse) {
return $ret;
}
return $this->redirect($this->generateUrl('home'));
}
private function makeApiRequest(\Facebook $facebook, $url, $method, $parameters)
{
try {
return $facebook->api($url, $method, $parameters);
} catch (\FacebookApiException $e) {
// https://developers.facebook.com/docs/graph-api/using-graph-api/#errors
if ($e->getType() == 'OAuthException' || in_array($e->getCode(), array(190, 102))) {
// our token is bad - reauthorize to get a new token
return $this->redirect($this->generateUrl('facebook_authorize_start'));
}
// it failed for some odd reason...
throw $e;
}
}
This method does the exact same thing as before. The if
statement checks
to see if makeApiRequest()
needs us to redirect the user back to the authorize
URL.
But if we add a new $retry
argument, we could run the request 1 more time if it fails:
private function makeApiRequest(\Facebook $facebook, $url, $method, $parameters, $retry = true)
{
try {
return $facebook->api($url, $method, $parameters);
} catch (\FacebookApiException $e) {
// ... the check for an expired token
// re-try one time
if ($retry) {
return $this->makeApiRequest($facebook, $url, $method, false);
}
// it failed for some odd reason...
throw $e;
}
}
Of course, this is really only interesting if we expect Facebook to have a decent number of temporary failures. But the big idea is that you should do your best to figure out why a failure has happened and re-try if it makes sense.
Tip
If you're using the Guzzle library to make API requests (which the Facebook class does not use), it has built-in support for re-trying a request if it fails. See Guzzle Retry Subscriber (for Guzzle version 4).
This is especially useful in the world of OAuth. We didn't store the Facebook access token in the database. But if we had, we could use it right now and re-try the request again:
private function makeApiRequest(\Facebook $facebook, $url, $method, $parameters, $retry = true)
{
try {
return $facebook->api($url, $method, $parameters);
} catch (\FacebookApiException $e) {
if ($e->getType() == 'OAuthException' || in_array($e->getCode(), array(190, 102))) {
if ($retry) {
$user = $this->getLoggedInUser();
// this is fake code - we don't have a facebookAccessToken
// property in our example project
$facebook->setAccessToken($user->facebookAccessToken);
return $this->makeApiRequest($facebook, $url, $method, false);
}
// ... the same redirect code
}
// ... the same throw code
}
}
So if the access token were missing from the session and the one in the database hasn't expired, this will make everything work perfectly smooth. Since this is fake code, let's remove all the retry code for now:
private function makeApiRequest(\Facebook $facebook, $url, $method, $parameters)
{
try {
return $facebook->api($url, $method, $parameters);
} catch (\FacebookApiException $e) {
if ($e->getType() == 'OAuthException' || in_array($e->getCode(), array(190, 102))) {
// our token is bad - reauthorize to get a new token
return $this->redirect($this->generateUrl('facebook_authorize_start'));
}
// it failed for some odd reason...
throw $e;
}
}
Finally, let's make it so the farmers can login with their Facebook account. Let's start by adding a link on the login page. Just like with "Login with COOP", the URL is to the page that starts the Facebook authorization process:
{# views/user/login.twig #}
{# ... #}
<button type="submit" class="btn btn-primary">Login!</button>
OR
<div class="btn-group">
<a href="{{ path('coop_authorize_start') }}" class="btn btn-default">
Login with COOP
</a>
<a href="{{ path('facebook_authorize_start') }}" class="btn btn-default">
Login with Facebook
</a>
</div>
Logging in with Facebook is going to work exactly like logging in with COOP. In fact, let's just copy all the related code from CoopOAuthController into our FacebookOAuthController:
// src/OAuth2Demo/Client/Controllers/FacebookOAuthController.php
// ...
public function receiveAuthorizationCode(Application $app, Request $request)
{
$facebook = $this->createFacebook();
$userId = $facebook->getUser();
// ...
if ($this->isUserLoggedIn()) {
$user = $this->getLoggedInUser();
} else {
$user = $this->findOrCreateUser($json);
$this->loginUser($user);
}
$user->facebookUserId = $userId;
$this->saveUser($user);
// ...
}
private function findOrCreateUser(array $meData)
{
if ($user = $this->findUserByCOOPId($meData['id'])) {
return $user;
}
if ($user = $this->findUserByEmail($meData['email'])) {
return $user;
}
$user = $this->createUser(
$meData['email'],
'',
$meData['firstName'],
$meData['lastName']
);
return $user;
}
But to create a user, we need some basic information, like email, first name
and last name. With COOP, we made an API request to get this information.
Let's do the same thing for Facebook, using the really important endpoint
/me
. And knowing that things can fail, let's make sure to wrap it in
a try-catch block:
public function receiveAuthorizationCode(Application $app, Request $request)
{
// ...
try {
$json = $facebook->api('/me?fields=email,first_name,last_name');
} catch (\FacebookApiException $e) {
return $this->render('failed_token_request.twig', array('response' => $e->getMessage()));
}
var_dump($json);die;
// ...
}
Tip
Due to recent Facebook API changes, you now need to add ?fields=
to explicitly
ask for which fields you want.
At this point, we should have a valid access token, so if the request fails, something is very strange. That's why I'm showing an error page instead of redirecting them to re-authorize.
I'm dumping the result of the API request, so let's logout and try the process. But first, reset the database so that it doesn't find our existing user:
rm data/topcluck.sqlite
When we login with Facebook, we hit the dump, which holds a lot of nice information about the user:
array (size=12)
'id' => string '100002910877036' (length=15)
'name' => string '...' (length=17)
'first_name' => string '...' (length=10)
'last_name' => string '...' (length=6)
...
We're allowed to ask for this information because when we redirect the user
for authorization, we're asking for the email
scope. Let's update the
findOrCreateUser()
method to use this data.
First, change findUserByCOOPId()
to findUserByFacebookId()
, which is
a shortcut method in my app to find a user by the facebookUserId()
column:
private function findOrCreateUser(array $meData)
{
if ($user = $this->findUserByFacebookId($meData['id'])) {
// this is an existing user. Yay!
return $user;
}
// ...
}
Next, change the firstName
and lastName
keys to match Facebook's
API response:
private function findOrCreateUser(array $meData)
{
// ...
$user = $this->createUser(
$meData['email'],
// a blank password - this user hasn't created a password yet!
'',
$meData['first_name'],
$meData['last_name']
);
return $user;
}
It's that easy! Go back to the login page and try the whole process. When it finishes, we can click on the "User Info" section to see that we're logged in as a new user.
And that's it! Since Facebook uses OAuth, working with it is almost exactly like working with COOP. The biggest differene is that Facebook has a PHP SDK, which makes life easier, but hides some of the OAuth magic that's happening behind the scenes. But now that you truly understand things, that's no problem for you!
Hey Michael,
Yes, you're right, this course is kinda old already but OAuth concepts we covered in it are still relevant. Unfortunately, we don't have any updates yet.
Cheers!
I'm trying to use the new php sdk. 3.2.3 version tell me that is deprecated. But I missed something. I don't know what. I ca't obtain access token in shareProgressOnFacebook function. I'm using the same code for all two functions from FacebookOauthController that requires $accessToken. I haven't any problem for receiveAuthorizationCode function but in shareProgressOnFacebook if i try to var_dump accessToken it's null. I used https://developers.facebook.com/docs/php/FacebookRequest/5.0.0 code to replicate the older code but with no luck. I have no idea how to correct things.
My code is like so:
public function shareProgressOnFacebook(Request $request){
$facebook = $this->createFacebook();
$helper = $facebook->getRedirectLoginHelper();
$accessToken = $this->handleFailedTokenRequestORGetAccessToken($helper, $request);
var_dump($accessToken);die(); //here strangely $accessToken is null, in receiveAuthorizationCode with exactly the same three lines is not
$fbApp = new FacebookApp(
getenv('FACEBOOK_APP_ID'),
getenv('FACEBOOK_APP_SECRET')
);
$user = $this->getLoggedInUser();
$facebookRequest = new FacebookRequest(
$fbApp,
$accessToken,
'POST',
'/'.$user->facebookUserId . '/feed',
array(
'message' => 'TEST'
)
);
try {
$response = $facebook->getClient()->sendRequest($facebookRequest);
var_dump($response);die();
} catch (FacebookResponseException $e) {
$errorBody = 'Graph returned an error ' . $e->getMessage();
return $this->render('failed_authorization.twig', array(
'response' => $request->query->all(),
'error_body' => $errorBody
));
} catch (FacebookSDKException $e){
$errorBody = 'Facebook SDK returned an error ' . $e->getMessage();
return $this->render('failed_authorization.twig', array(
'response' => $request->query->all(),
'error_body' => $errorBody
));
}
return $this->redirect($this->generateUrl('home'));
}
and function handleFailedTokenRequestORGetAccessToken is like so:
private function handleFailedTokenRequestORGetAccessToken(FacebookRedirectLoginHelper $helper, Request $request){
try{
$accesToken = $helper->getAccessToken();
} catch (FacebookResponseException $e) {
return $this->render('failed_token_request.twig', array(
'response' => $request->query->all(),
'error_message' => $e->getMessage()
));
} catch (FacebookSDKException $e){
return $this->render('failed_token_request.twig', array(
'response' => $request->query->all(),
'error_message' => $e->getMessage()
));
}
if (!isset($accesToken)){
if ($helper->getError()){
$error_body = 'Error: ' . $helper->getError() . '\n';
$eror_body .= 'Error Code: ' . $helper->getErrorCode() . '\n';
$eror_body .= 'Error Reason: ' . $helper->getErrorReason() . '\n';
$eror_body .= 'Error Description: ' . $helper->getErrorDescription() . '\n';
return $this->render('failed_authorization.twig', array(
'response' => $request->query->all(),
'error_body' => $eror_body
));
} else {
$eror_body = 'Bad Request';
return $this->render('failed_authorization.twig', array(
'response' => $request->query->all(),
'error_body' => $eror_body
));
}
}
return $accesToken;
}
Hi Diaconescu!
Sorry for my slow reply! Ok, if I understand things correctly, you can get the access token from inside receiveAuthorizationCode
, correct? But you cannot get it from inside of shareProgressOnFacebook
.
If that's correct, here's the reason: you can only fetch the access token from inside of receiveAuthorizationCode
. Specifically, you can only fetch the access token right after Facebook redirects back to your site. That's because when this happens, Facebook adds a ?code= to your URL. When you call getAccessToken()
, my guess is that this function reads the ?code= parameter and uses it to fetch the access token. That's native to how OAuth works: you're exchanging the authorization code (?code=) for an access token. You cannot fetch an access token at any other time.
But, that's ok! When you fetch the access token in receiveAuthorizationCode
, you just need to store it in the database or on the user's session so that you can use it later. In other words, save the access token so that you can use it in shareProgressOnFacebook
. Just know that the access tokens are not permanent. If using the access token fails, you'll need to send your user back through the OAuth flow to get a fresh access token.
Cheers!
Got stuck with the following:
$json = $facebook->api('/me');
var_dump($json);die();
On clicking 'Login with FB' returns only
array (size=2)
'name' => string '{My Name}' (length=14)
'id' => string '#' (length=17)
No e-mail or other data.
Is it a problem connected to the old SDK or smth else?
Hi Mihail!
Hmm, I'm not sure. I would verify that you're sending the right scopes when you redirect to FB. It's possible you're not asking for any scopes, so you're *only* getting back the name + id. Btw, is the id literally '#'? That definitely seems weird.
It's possible that the old SDK finally doesn't work with the Facebook graph API, but I haven't heard of any other issues (yet).
Cheers!
Hi Ryan!
Thank you for your reply!
Id is fine, I just used dump symbol.
As far as I understand the scope is set right, according to the tutorial in FacebookOAuthController::redirectToAuthorization()
$url = $facebook->getLoginUrl(array(
'redirect_uri' => $redirectUrl,
'scope' => array('email', 'publish_actions')
));
Hi Mihail!
Hmm, yes, I just tried it again with the real code that's included in the code download and it *does* work as expected. When I dump the $json, I'm returned an array with id, email, first_name, gender, name and about 5 other fields. It's possible that when you originally authorized your Facebook application for your account, you only authorized it for 1 scope (but not all the scopes we were asking for). You can remove authorization for your app (https://www.facebook.com/se... and try it again - that might fix it.
Cheers!
I have the same issue as Mihail.
Only info I get from FaceBook is name & id.
I've tried to re-authorize Facebook app with no luck.
Also when authorizing Facebook app, I get a message:
"Submit for Login Review
Some of the permissions below have not been approved for use by Facebook.
Submit for review now or learn more."
which wasn't shown in tutorial.
Screenshot: http://cl.ly/1i0t1K2r3a0o
Hey Kuba!
Ah, fascinating! Your screenshot might lead to the solution! It seems that Facebook now requires your application to be "reviewed" by them in order to ask for *certain* scopes. If you ask for the "email" or "public_profile" scope, then your app "Top Cluck" does not need to be reviewed. But if you ask for publish_actions (as we do), then your app *does* need to be reviewed. I'll see if I can get them to review our very awesome TopCluck app :).
So, if you do *not* ask for the publish_actions scope, do you then get back more information?
Cheers!
For anyone else who winds up here, simply adding the fields wasn't enough for me as I still couldn't get access to "email". I'd get first_name, last_name, and ID. To get email, I had to make a new "test user" in Facebook's app page. It is my understanding you may not retrieve all information from real users prior to app review, but test users will work regardless.
Hmm what Facebook PHP package are you using? I'm using "facebook/graph-sdk": "^5.4", and I haven't seen these methods/services/exceptions
Hey Gytis,
For the client application in this screencast we use "facebook/php-sdk" v3.2.3.
Cheers!
Is there an updated version of this course or code? It uses Symfony 2.4, though it seems that many of the concepts of authorization are the same in Symfony 4. Still, if there's something newer...