Chapters
This course is archived!
While the concepts of this course are still largely applicable, it's built using an older version of Symfony (4) and React (16).
-
Course Code
Subscribe to download the code!Compatible PHP versions: ^7.2.0
Subscribe to download the code!Compatible PHP versions: ^7.2.0
-
This Video
Subscribe to download the video!
Subscribe to download the video!
-
Subtitles
Subscribe to download the subtitles!
Subscribe to download the subtitles!
-
Course Script
Subscribe to download the script!
Subscribe to download the script!
API Auth & State via AJAX
Scroll down to the script below, click on any sentence (including terminal blocks) to jump to that spot in the video!
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeWhen we used fetch()
to make an AJAX call to /reps
... we were "rewarded" with this big ugly error. This tells me that fetch is probably having problems... for some reason... parsing the JSON in the response.
Let's go see what happened! Click on the Network tab in your browser tools and filter for only XHR requests. Ah, here is one for /reps
that was successful. BUT! That's the wrong AJAX call: this is the AJAX call made by our old code. So... where the heck is the other /reps
AJAX call that was just made by fetch()
?
Click instead to filter by the "Other" tab. There it is! Why is it here? Well... because... something went wrong. Look at the response: 302. And if you look at the response headers... woh! It is a redirect to the login page, which is why you see a second request below for /login
.
Let's back up. First, for some reason, authentication is failing for the API request. We'll get to that in a minute. Second, fetch()
requests will normally show up under the XHR network filter. We'll see that later. But, if something goes wrong, the request may show up under "Other". Just be aware of that: it's a gotcha!
Sending the Cookie Headers
So, why the heck is authentication failing? If we go directly to /reps
, it works! What's wrong with you fetch!
This, in my opinion, is one of the really cool things about fetch. Look at our controller. Ah, every endpoint requires us to be logged in! This works in our browser because our browser automatically sends the session cookie. But fetch()
, on the other hand, does not automatically send any cookies when it makes a request.
What Type of Authentication to Use?
I like this because it forces you to ask yourself:
Hey, how do I want to authenticate my API requests?
API authentication is a big topic. So we're going to skip it! I'm kidding: it's too important.
One way or another, every API request that needs authentication will have some sort of authentication data attached to it - maybe a session cookie or an API token set on a header.
So... what type of authentication should you use for your API? Honestly, if you're building an API that will be consumed by your own JavaScript front-end, using session cookies is an awesome option! You don't need anything fancier. When we login, that sets a session cookie. In a moment, we'll tell fetch to send that cookie, and everything will be solved. If you want to build your login page in React and send the username and password via AJAX, that's totally fine: when your server sends back the session cookie, your browser will see it & store it. Well, as long as you use the credentials
option that I'm about to show you for that AJAX call.
Of course, if you want, you can also create an API token authentication system, like JWT or OAuth. That's totally fine, but that truly is a separate topic.
Whatever you choose, when it's time to make your API call, you will attach the authentication info to the request: either by sending the session cookie or your API token as a header.
Sending the Session Cookie
To send the session cookie, fetch has a second options argument. Add credentials
set to same-origin
. Thanks to this, fetch()
will send cookies to any requests made back to our domain.
Show Lines
|
// ... lines 1 - 6 |
return fetch('/reps', { | |
credentials: 'same-origin' | |
}) | |
Show Lines
|
// ... lines 10 - 14 |
Tip
The default value of credentials
may change to same-origin
in the future.
Ok, let's see if this fixes things! Move over and refresh. No errors! Check out the console. Yes! There is our data! Notice, the API wraps everything inside an items
key. Yep, inside: the 4 rep logs, which have the same fields as the state in our app. That was no accident: when we added the static data, I made sure it looked like the real data from the API so that we could swap it out later.
Processing the fetch data via Promises
In rep_log_api
, I really want my getRepLogs()
API to return a Promise that contains the array of rep logs... without that items
key. To do that, it's a bit weird. The .json()
method returns another Promise. So, to do further processing, chain a .then()
from it and, inside the callback, return data.items
.
Show Lines
|
// ... lines 1 - 9 |
.then(response => { | |
return response.json().then((data) => data.items) | |
}); | |
Show Lines
|
// ... lines 13 - 14 |
Promises on top of promises! Yaaaay! When fetch()
finishes, it executes our first callback. Then, when the JSON decode finishes, it executes our second callback, where we read off the .items
key. Ultimately, getRepLogs()
returns a Promise
object where the data is the array of rep logs. Phew!
And because the browser already refreshed while I was explaining all of the promises, yep! You can see the logged data is now the array.
componentDidMount
Awesome! Let's use this to set our initial state! First, set the initial repLogs
state to an empty array. Next, copy the getRepLogs()
call and remove it. Instead, create a new method called componentDidMount()
and paste this there. In the callback, use this.setState()
to set repLogs
to data
.
Show Lines
|
// ... lines 1 - 22 |
componentDidMount() { | |
getRepLogs() | |
.then((data) => { | |
this.setState({ | |
repLogs: data | |
}) | |
}); | |
} | |
Show Lines
|
// ... lines 31 - 83 |
Before we talk about this, let's try it. Refresh! Woh! We have real data! Yes, yes, yes! We're showing the same data as our original app!
Back to the code! Until this moment, render()
was the only "special", React-specific, method in our class. But there are a few other special methods called "lifecycle" methods. The componentDidMount()
method is one of those: if this exists, React calls it right after our component is rendered to the DOM. And this is the best place to make any AJAX requests needed to populate your initial state.
Actually, we could have left this code in the constructor()
. Because we're in a browser, they're almost the same. But, componentDidMount()
is generally the recommended place.
21 Comments
Hey Stephane
Thanks for the info man! probably in a couple of years all browsers will do the same
Cheers!
Excellent!
Hi!
I'm following along but with a slightly different table structure. I have a table books, and each book can have many notes. When I try to serialize this I get the error:
"A circular reference has been detected when serializing the object of class "App\Entity\Note""
Following the docs, I have tried to update it to this:
`
$json = $this->serializer->serialize($data, 'json', [
AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER => function ($object, $format, $context) {
return $object->getId();
}
]);
`
But this just leads to a 30 second time-out error. Any ideas where I'm going wrong here?
Hey @HimmelHempsted
Sorry for late answer! I think you should configure @MaxDepth()
annotation, on your Entities, Not sure about you structure, you should try to add it to $books
or $notes
properties, try to play with it to get best result for your structure!
Cheers!
Hey Guys,
I have run into a head scratcher. If I execute a fetch call to the api, then the data I get back is the html for the Symfony profiler debug toolbar.
If I make the same call twice, then both calls have the json data that I was expecting.
If I disable the profiler, then I get the correct data.
In the logs, I can see that the single call was route matched to the profiler, but I don't know why. Have you ever run into this problem?
Hey Skylar
That's funny. How looks your controller's API endpoint?
When you get the profiler's html as response did the request fail?
Cheers!
When it fails, I look at the last 10 requests, and it is NOT in the list. But if I look the inspector tools in chrome, it shows that it was sent. I also added a random number to the query string to do cache busting in case that was a possibility.
Hmm, that's indeed weird. Which web server are you using? I believe there is something funny on your configuration
Are you using a HTTPS connection? I don't really know what's going on but could you try starting your web server by runningbin/console server:start
and try again?
So I solved my problem by replacing the Symfony Built-in server with apache using xampp. All the problems went away. So, my guess is that there must have been a timing issue using the built-in server. go figure.
Hmm, that's super weird. You should be able to run your Symfony app through Symfony web-server. Probably there is an issue related to something specific to your environment. I would try re-installing Symfony CLI but well... at least you can keep going on :)
Cheers!
I got an problem on this.
I don't have a "reps" request in the [other] type of the network tab, nor "login". Also the error in the console says "...at position 110", not 0.
I decided to carry on but the predicted fix (adding the authentication argument to the fetch) doesn't change anything.
Any idea ? (I noticed you often reference the course on ES6. I didn't do it, so I may have missed something)
Hey Johann I.
So, your browser is not executing any requests to "reps" endpoint? It is very likely that your application is not performing that requests on page load, i.e. on "componentDidMount" method. Double check that your app is indeed executing that request
Cheers!
After some time inactivity in browser in response got redirection to login page, how to deal with it?
Hey bartek!
It depends on what you want the behavior to be. The easiest solution would be to increase Symfony's session length, or use a "remember_me" cookie (you can even automatically turn this on) so that users are basically *not* logged out. Or, you could create a heartbeat AJAX request that is sent every 10 minutes to maintain a session with the server. Or, if you *do* want to allow people to be logged out, and you want present them with the login form, it's a bit tricker. You'll need to make sure that all of your AJAX/API calls go through a central function, and in that central function you'll call fetch(). You'll need to attach an error handler that looks for authentication failure (I would also make your authentication system return a 401 response + JSON when you need to login, at least if it detects if it's an AJAX request. How you do this depends on how you've built your authentication system) and then opens a React component to show the login page. You'll also need to "save" the original request details so that you can re-send them after success.
I know this is a very generic answer - but there are multiple ways to handle it depending on what you need :).
Cheers!
Hi! I have a question regarding this. You are saying to have a central function for all ajax request to check if user is authentificated. So authentification checking is done only when ajax call is made. What if I want also check if the session expired or not when I am not active on the page and its automatically redirects to login page? My login page is doen in React. Should I have a function to periodically check the session expired or not in may entry file App? Thanks in advance
Hey Assel N.
If you want to check if the session has expired or not at the front-end level, then yes, you will have to execute a function periodically. Now, the function doesn't have to live in the main App file, it depends on your structure but that's something you can re-arrange when the time comes.
Cheers!
"Houston: no signs of life"
Start the conversation!
What PHP libraries does this tutorial use?
// composer.json
{
"require": {
"php": "^7.2.0",
"ext-iconv": "*",
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"doctrine/annotations": "^1.0", // v1.8.0
"doctrine/doctrine-bundle": "^1.6", // 1.9.1
"doctrine/doctrine-cache-bundle": "^1.2", // 1.3.3
"doctrine/doctrine-fixtures-bundle": "~3.0", // 3.0.2
"doctrine/doctrine-migrations-bundle": "^1.2", // v1.3.1
"doctrine/orm": "^2.5", // v2.7.2
"friendsofsymfony/jsrouting-bundle": "^2.2", // 2.2.0
"friendsofsymfony/user-bundle": "dev-master#4125505ba6eba82ddf944378a3d636081c06da0c", // dev-master
"phpdocumentor/reflection-docblock": "^3.0|^4.0", // 4.3.0
"sensio/framework-extra-bundle": "^5.1", // v5.2.0
"symfony/asset": "^4.0", // v4.1.4
"symfony/cache": "^3.3|^4.0", // v4.1.4
"symfony/console": "^4.0", // v4.1.4
"symfony/flex": "^1.0", // v1.21.6
"symfony/form": "^4.0", // v4.1.4
"symfony/framework-bundle": "^4.0", // v4.1.4
"symfony/lts": "^4@dev", // dev-master
"symfony/monolog-bundle": "^3.1", // v3.3.0
"symfony/polyfill-apcu": "^1.0", // v1.9.0
"symfony/property-access": "^3.3|^4.0", // v4.1.4
"symfony/property-info": "^3.3|^4.0", // v4.1.4
"symfony/serializer": "^3.3|^4.0", // v4.1.4
"symfony/swiftmailer-bundle": "^3.1", // v3.2.3
"symfony/twig-bundle": "^4.0", // v4.1.4
"symfony/validator": "^4.0", // v4.1.4
"symfony/yaml": "^4.0", // v4.1.4
"twig/twig": "2.10.*" // v2.10.0
},
"require-dev": {
"easycorp/easy-log-handler": "^1.0.7", // v1.0.7
"symfony/debug-bundle": "^3.3|^4.0", // v4.1.4
"symfony/dotenv": "^4.0", // v4.1.4
"symfony/maker-bundle": "^1.5", // v1.5.0
"symfony/phpunit-bridge": "^4.0", // v4.1.4
"symfony/stopwatch": "^3.3|^4.0", // v4.1.4
"symfony/var-dumper": "^3.3|^4.0", // v4.1.4
"symfony/web-profiler-bundle": "^3.3|^4.0", // v4.1.4
"symfony/web-server-bundle": "^4.0" // v4.1.4
}
}
What JavaScript libraries does this tutorial use?
// package.json
{
"dependencies": {
"@babel/plugin-proposal-object-rest-spread": "^7.12.1" // 7.12.1
},
"devDependencies": {
"@babel/preset-react": "^7.0.0", // 7.12.5
"@symfony/webpack-encore": "^0.26.0", // 0.26.0
"babel-plugin-transform-object-rest-spread": "^6.26.0", // 6.26.0
"babel-plugin-transform-react-remove-prop-types": "^0.4.13", // 0.4.13
"bootstrap": "3", // 3.3.7
"copy-webpack-plugin": "^4.4.1", // 4.5.1
"core-js": "2", // 1.2.7
"eslint": "^4.19.1", // 4.19.1
"eslint-plugin-react": "^7.8.2", // 7.8.2
"font-awesome": "4", // 4.7.0
"jquery": "^3.3.1", // 3.3.1
"promise-polyfill": "^8.0.0", // 8.0.0
"prop-types": "^15.6.1", // 15.6.1
"react": "^16.3.2", // 16.4.0
"react-dom": "^16.3.2", // 16.4.0
"sass": "^1.29.0", // 1.29.0
"sass-loader": "^7.0.0", // 7.3.1
"sweetalert2": "^7.11.0", // 7.22.0
"uuid": "^3.2.1", // 3.4.0
"webpack-notifier": "^1.5.1", // 1.6.0
"whatwg-fetch": "^2.0.4" // 2.0.4
}
}
Since Aug 25, 2017. The spec changed the default credentials policy to same-origin for fetch in Firefox changed since 61.0b13.