Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Cart API & Data

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.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Ok team! We have two great pages - a product catalog and a product show page. We're ready to start adding products to our cart.

To help with this, in the tutorial/ directory - which you should have if you download the course code - copy the cart-service.js file into assets/services/.

The Cart API Endpoints & Service

Just like product, category and color, "cart" is one of the "resources" in our API. Check it out: go to http://localhost:8000/api. Yep, we have a set of endpoints to create, view or modify a cart. And each cart has an "id".

Now, on the server, we're storing the current user's cart ID in the session. And if the user does currently have a cart, we're dumping its ID onto the page as a global variable. Check it out: right click in the browser, go to "View Page Source" and search for "cart". There we go: window.cartIri = null because - right now - we do not have a cart associated with our session yet.

The cart-service.js file holds a number of functions for fetching the cart, adding items to the cart and so on. To get the cart id, this calls getCartIrI(), which reads that global variable:

... lines 1 - 14
/**
* Gets the Cart IRI or null if there is none
*
* @return {string|null}
*/
function getCartIri() {
return window.cartIri;
}
... lines 23 - 149

Back in addItemToCart():

... lines 1 - 61
/**
* Adds an item to the cart and saves it
*
* @param {CartCollection} cart
* @param {CartItem} item
* @return {Promise}
*/
export async function addItemToCart(cart, item) {
... lines 70 - 79
if (cartIri !== null) {
response = await axios.put(cartIri, cart);
} else {
response = await axios.post('/api/carts', cart);
setCartIri(response.data['@id']);
}
... lines 86 - 87
}
... lines 89 - 149

this is smart enough to work even if the user doesn't have a cart yet. If the cartIri is not null, it updates it. But if it is null, it creates a new cart, which our API will automatically associate with the current user's session.

By the way, the cart data itself is also stored in the session instead of the database. That fact is completely not important for us in Vue. I just mention it in case you're an API Platform geek and want to see how that was implemented. In a real app, I would store carts in the database.

So from a JavaScript perspective, the important thing is that we have a traditional set of API endpoints for our cart and the current user's cart IRI is available to us on page load.

Now, here's the plan: in product-show, we're going to first load the full cart data via an Ajax call if there is one. Once we have that, we'll use it to add new items to the cart. Wait... but why do we need to fetch the cart data in order to add an item to it?

The reason is... well... in part because I'm trying to make our life difficult. But that's not the entire reason. Look at addItemToCart:

... lines 1 - 61
/**
* Adds an item to the cart and saves it
*
* @param {CartCollection} cart
* @param {CartItem} item
* @return {Promise}
*/
export async function addItemToCart(cart, item) {
const cartIri = getCartIri();
const itemIndex = findItemIndex(cart, item.product, item.color);
if (itemIndex !== -1) {
cart.items[itemIndex].quantity += item.quantity;
} else {
cart.items.push(item);
}
... lines 78 - 87
}
... lines 89 - 149

the cart object is the first argument. That's needed so that if we add a product to the cart and it's already in the cart, it can read the existing quantity and increase it. Now, we could have moved this smartness into our API, which would make our life easier here. But since we don't always have that luxury in the real world, we'll handle it in JavaScript.

Oh, and speaking of complications, we could also make our life simpler by rendering the entire cart object as the global variable... instead of just the IRI. In templates/base.html.twig, at the bottom, this where we're setting the cartIri variable. If we output the entire cart as JSON, then we could avoid an AJAX call in Vue. I probably would do that in a real app. But for the tutorial, this AJAX call is going to complicate things in wonderful ways.

<!DOCTYPE html>
<html lang="en-US">
... lines 3 - 9
<body>
... lines 11 - 59
{% block javascripts %}
<script>
{% if app.session.has('_cart_id') %}
window.cartIri = '{{ app.session.get('_cart_'~app.session.get('_cart_id'))|iri }}';
{% else %}
window.cartIri = null;
{% endif %}
</script>
{{ encore_entry_script_tags('app') }}
{% endblock %}
</body>
</html>

Fetching the Cart via AJAX

Anyways, let's get to work! Inside product-show.vue, we need the cart object so that we can use it when an item is added to the cart. Down in data, add a new key to store this: cart.

... lines 1 - 63
<script>
... lines 65 - 71
export default {
name: 'ProductShow',
... lines 74 - 84
data() {
return {
cart: null,
... lines 88 - 89
};
},
... lines 92 - 111
};
</script>
... lines 114 - 131

Next, in created(), call one of the new functions - fetchCart() and let PhpStorm auto-complete that: we want the one from assets/. But, hmm: let's make sure PhpStorm stops looking at the tutorial/ directory: right click on it and go to "Mark Directory as Excluded".

Anyways, when I auto-completed that, PhpStorm added the fetchCart import for us. Back in create, since fetchCart() returns a Promise, we can say .then() and pass a callback with a cart argument. Inside, all we need is this.cart = cart.

... lines 1 - 63
<script>
import { fetchCart } from '@/services/cart-service.js';
... lines 66 - 71
export default {
name: 'ProductShow',
... lines 74 - 100
async created() {
fetchCart().then((cart) => {
this.cart = cart;
});
... lines 105 - 110
},
};
</script>
... lines 114 - 131

If you're wondering why I didn't use await, good... wondering! If we had used await, it would mean that this first AJAX call would need to finish before the second one could even start. By using .then(), both AJAX calls will effectively start instantly.

Ok! Let's make sure the data loads! Refresh the page, go over to the Vue Dev tools, Components... and find ProductShow. Yes! We have a cart data set to an object with an items key. It's empty because, in reality, we don't have a cart yet. But the fetchCart() function is nice enough to create an empty object for us in this case.

Next, let's hook up the "Add to Cart" button and make sure the user cannot add any items until we're ready.

Leave a comment!

4
Login or Register to join the conversation
Cecile Avatar

Hi,
I have this error :
The filter `iri` not found in `base.html.twig`
window.cartIri = '{{ app.session.get('_cart_'~app.session.get('_cart_id'))|iri }}';
When I check 'src/twig' indeed it is not defined!
thanks

Reply

Hey Cecile!

If you download the course code form this page, this iri filter comes from a custom Twig extension that we include in that code :). Just in case, you can find the source for the twig extension here: https://gist.github.com/wea...

Let me know if that helps!

Cheers!

Reply
Cecile Avatar

thanks @weaverryan soo much, this helps me, because I used the code from part 1

Reply

Ah yes, sorry for changing a few things! I know that an be inconvenient as a watcher... but it's really useful for us to add some "extra: stuff between tutorials so that we can keep moving, but not cover boring details (like aa Twig extension in a Vue course). There are various other things that have been tweaked - so you might want to use fresh code from part 2... or beware ;).

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This course is also built to work with Vue 3!

What JavaScript libraries does this tutorial use?

// package.json
{
    "devDependencies": {
        "@fortawesome/fontawesome-free": "^5.15.1", // 5.15.1
        "@symfony/webpack-encore": "^0.30.0", // 0.30.2
        "axios": "^0.19.2", // 0.19.2
        "bootstrap": "^4.4.1", // 4.5.3
        "core-js": "^3.0.0", // 3.6.5
        "eslint": "^6.7.2", // 6.8.0
        "eslint-config-airbnb-base": "^14.0.0", // 14.2.0
        "eslint-plugin-import": "^2.19.1", // 2.22.1
        "eslint-plugin-vue": "^6.0.1", // 6.2.2
        "regenerator-runtime": "^0.13.2", // 0.13.7
        "sass": "^1.29.0", // 1.29.0
        "sass-loader": "^8.0.0", // 8.0.2
        "vue": "^2.6.11", // 2.6.12
        "vue-loader": "^15.9.1", // 15.9.4
        "vue-template-compiler": "^2.6.11", // 2.6.12
        "webpack-notifier": "^1.6.0" // 1.8.0
    }
}