Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
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).

The POST Create API

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

We now GET and DELETE the rep logs via the API. The last task is to create them when the form submits. Look back at RepLogController: we can POST to /reps to create a new rep log. I want to show you just a little bit about how this works.

About the POST API Code

The endpoint expects the data to be sent as JSON. See: the first thing we do is json_decode the request content. Then, we use Symfony's form system: we have a form called RepLogType with two fields: reps and item. This is bound directly to the RepLog entity class, not the model class.

Using the form system is optional. You could also just use the raw data to manually populate a new RepLog entity object. You could also use the serializer to deserialize the data to a RepLog object.

These are all great options, and whatever you choose, you'll ultimately have a RepLog entity object populated with data. I attach this to our user, then flush it to the database.

For the response, we always serialize RepLogApiModel objects. So, after saving, we convert the RepLog into a RepLogApiModel, turn that into JSON and return it.

I also have some data validation above, which we'll handle in React later.

Fetching to POST /reps

To make the API request in React, start, as we always do, in rep_log_api.js. Create a third function: export function createRepLog. This needs a repLog argument, which will be an object that has all the fields that should be sent to the API.

... lines 1 - 25
export function createRepLog(repLog) {
... lines 27 - 33
}

Use the new fetchJson() function to /reps with a method set to POST. This time, we also need to set the body of the request: use JSON.stringify(repLog). Set one more option: a headers key with Content-Type set to application/json. This is optional: my API doesn't actually read or care about this. But, because we are sending JSON, it's a best-practice to say this. And, later, our API will start requiring this.

... lines 1 - 26
return fetchJson('/reps', {
method: 'POST',
body: JSON.stringify(repLog),
headers: {
'Content-Type': 'application/json'
}
});

Ok, API function done! Head back to RepLogApp and scroll up: import createRepLog. Then, down in handleAddRepLog, use it! createRepLog(newRep). To see what we get back, add .then() with data. console.log() that.

... lines 1 - 4
import { getRepLogs, deleteRepLog, createRepLog } from '../api/rep_log_api';
... line 6
export default class RepLogApp extends Component {
... lines 8 - 37
handleAddRepLog(itemLabel, reps) {
... lines 39 - 45
createRepLog(newRep)
.then(data => {
console.log(data);
})
;
... lines 51 - 56
}
... lines 58 - 88
}
... lines 90 - 93

Well... let's see what happens! Move over and refresh. Okay, select "Big Fat Cat", 10 times and... submit! Boo! The POST failed! A 400 error!

Matching the Client Data to the API

Go check it out. Interesting... we get an error that this form should not contain extra fields. Something is not right. In Symfony, you can look at the profiler for any AJAX request. Click into this one and go to the "Forms" tab. Ah, the error is attached to the top of the form, not a specific field. Click ConstraintViolation to get more details. Oh... this value key holds the secret. Our React app is sending id, itemLabel and totalWeightLifted to the API. But, look at the form! The only fields are reps and item! We shouldn't be sending any of these other fields!

Actually, itemLabel is almost correct. It should be called item. And instead of being the text, the server wants the value from the selected option - something like fat_cat.

Ok, so we have some work to do. Head back to RepLogApp. First: remove the stuff we don't need: we don't need id and we're not responsible for sending the totalWeightLifted. Then, rename itemLabel to item. Rename the argument too, because this now needs to be the option value.

... lines 1 - 37
handleAddRepLog(item, reps) {
const newRep = {
reps: reps,
item: item
};
... lines 43 - 54
}
... lines 56 - 91

This function is eventually called in RepLogCreator as onAddRepLog. Instead of text, pass value.

... lines 1 - 40
onAddRepLog(
itemSelect.options[itemSelect.selectedIndex].value,
... line 43
);
... lines 45 - 100

Updating State after the AJAX Call

In RepLogApp, newRep now contains the data our API needs! Woohoo! But... interesting. It turns out that, at the moment the user submits the form, we don't have all the data we need to update the state. In fact, we never did! We were just faking it by using a random value for totalWeightLifted.

This is a case where we can't perform an optimistic UI update: we can't update the state until we get more info back from the server. This is no big deal, it just requires a bit more work.

Comment out the setState() call.

... lines 1 - 37
handleAddRepLog(item, reps) {
... lines 39 - 49
// this.setState(prevState => {
// const newRepLogs = [...prevState.repLogs, newRep];
//
// return {repLogs: newRepLogs};
// })
}
... lines 56 - 91

Let's refresh and at least see if the API call works. Lift my big fat cat 55 times and hit enter. Yes! No errors! The console log is coming from the POST response... it looks perfect! Id 30, it returns the itemLabel and also calculates the totalWeightLifted. Refresh, yep! There is the new rep log!

Ok, let's update the state. Because our API rocks, we know that the data is actually a repLog! Use this.setState() but pass it a callback with prevState. Once again, the new state depends on the existing state.

... lines 1 - 37
handleAddRepLog(item, reps) {
... lines 39 - 43
createRepLog(newRep)
.then(repLog => {
this.setState(prevState => {
... lines 47 - 49
})
})
;
}
... lines 54 - 89

To add the new rep log without mutating the state, use const newRepLogs = an array with ...prevState.repLogs, repLog. Return the new state: repLogs: newRepLogs. Remove all the old code below.

... lines 1 - 44
.then(repLog => {
this.setState(prevState => {
const newRepLogs = [...prevState.repLogs, repLog];
return {repLogs: newRepLogs};
})
})
... lines 52 - 89

Let's try it! Make sure the page is refreshed. Lift our normal cat this time, 10 times, and boom! We've got it!

Using UUID's?

This was the first time that our React app did not have all the data it needed to update state immediately. It needed to wait until the AJAX request finished.

Hmm... if you think about it, this will happen every time your React app creates something through your API... because, there is always one piece of data your JavaScript app doesn't have before saving: the new item's database id! Yep, we will always need to create a new item in the API first so that the API can send us back the new id, so that we can update the state.

Again, that's no huge deal... but it's a bit more work, and it will require you to add more "loading" screens so that it looks like your app is saving. It's just simpler if you can update the state immediately.

And that is why UUID's can be awesome. If you configure your Doctrine entities to use UUID's instead of auto-increment ids, you can generate valid UUID's in JavaScript, update the state immediately, and send the new UUID on the POST request. The server would then make sure the UUID has a valid format and use it.

If you're creating a lot of resources, keep this in mind!

Leave a comment!

8
Login or Register to join the conversation
Anthony R. Avatar
Anthony R. Avatar Anthony R. | posted 4 years ago | edited

there Awesome post as usual! BTW I'm Premium user now! :) I have a not-so easy question in regard to UUIDs. I just wonder how you would recommend storing UUIDs as primary keys and whether you would know of any performance issues using UUIDs as primary keys. It sounds awesome but it's 128bits compared to 32bits auto incremented IDs. Any tips? For example, does Postgres handle it better than mySQL? Any resources that may guide us to use UUID (or anything similar) as primary keys? Thank you!

Reply

Hey Anthony R.

Congrats on being a premiun user!
About your question. I found this very interesting article about incremental ID's vs UUID's https://medium.com/@Mareks_082/auto-increment-keys-vs-uuid-a74d81f7476a
you may want to read it
If you are using MySql and Doctrine 2, this is how you can use UUID's as primary key


// SomeEntity.php
    /**
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="UUID")
     * @ORM\Column(name="id", type="guid")
     */
    private $id;

Cheers!

Reply
Remus M. Avatar
Remus M. Avatar Remus M. | posted 4 years ago

Hi Ryan, great course on React and Symfony 4! I followed it closely and applied the structure to my own app. I have a simple form for adding city names to a database. Everything is fine until I try to upload images for each city. I am struggling for hours to solve this and I can't, searched for a solution but nothing seems to work. I modified the newRepLogAction controller and read data like this: $data = $request->getContent(), but I always get 400 bad request from if (!$form->isValid()) . I suppose I don't need JSON anymore. I also modified the way I send the form data:

const formData = new FormData();
formData.append("field-name", cityName);
formData.append("file-field-name", cityImage);

and the fetch call:

return fetchJson('url', {
method: 'POST',
body: formData
});
In the profile I have this error: "This value is not valid" caused by: "ConstraintViolation" and "TransformationFailedException". Can you explain us in a few words how is different the file upload ? Thanks.

Reply

Hey Remus M.!

Excellent question! I have few points / questions that will hopefully help you out!

1) Where does the cityImage variable come from exactly? Are you reading it from the file input?
2) If you're sending data using FormData, then I believe that this looks like a normal "form submit" to Symfony. In other words, no, you're not sending things via JSON, and so you will not be reading things via $request->getContent(). Instead, you'll be reading things from the POST fields directly - e.g. $form->request->get('cityName'). However, if you're using the form system, then using $form->handleRequest($request) may not work either (depends on your setup) as Symfony forms usually expects your POST data to be under a namespace - e.g. field named like product[field-name] - not just field-name. However, the error you're getting doesn't quite make me think this is the problem.
3) When you submit, if you dump($request->files->all()) what does it look like?

Basically, for the form upload to work, we need to make sure that the data is being sent up to the server correctly, and that we're finding it in the correct location. In general, the "This value is not valid" is being caused by a "data transformer" on one of your fields (you can see exactly which field in the profiler - it may be attached to the "top level form" or a specific field - and exactly where the field is attached is important). Basically, many field types have built in "sanity" validation - and this is the validation error you're using. For example, if you have a "date" input field, and you submit a string that is not a valid date, you would see this error. In your case, it's a signal that some data is not being sent or received correctly. Again, exactly which field (or it could be on the form itself) has this error is important.

By the way, in a true JSON API, file uploads can be handled in a variety of different ways, but here are a few common examples:

1) Make an endpoint where you literally send ONLY the contents of the file as the entire body of the request. Then, $request->getContent() is literally the file contents. It's kind of a pure "API" endpoint to send data for single file. But, you can only send that one thing - you can't send 10 pieces of data, where 1 of them is a file.

2) Send a JSON string like normal, but for the "cityImage" field, make it be the base64 encoded content of your file. On the server, read this, base64 decode it and... you've got your file! You can see an example of this in GitHub's API: https://developer.github.com/v3/repos/contents/#create-a-file

Let me know what you find out - I hope this helps :).

Cheers!

Reply
Remus M. Avatar

I have tried the first solution and it works if I acces the file like this:
$data = $request->files->all();
dump($data["field-name"]); // object with all the file info

$request->getContent() is empty.

Thank you for help.

Reply

Yo Remus M.!

Hmm, interesting! Well, it all depends on exactly *how* your JavaScript upload code looks, but if you've got it working, GREAT!

Cheers!

Reply

At the end a this chapter, when I delete a replog, I have this error :
SyntaxError: JSON.parse: unexpected end of data at line 1 column 1 of the JSON data

This is normal. The solution will solve into 35 chapter.

Reply
Marcelo F. Avatar
Marcelo F. Avatar Marcelo F. | Stephane | posted 4 years ago | edited

Yep - sorry about that Stephane! This was an accident actually - I didn't discover my mistake until that chapter :).

Reply
Cat in space

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

While the concepts of this course are still largely applicable, it's built using an older version of Symfony (4) and React (16).

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/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
        "sensio/framework-extra-bundle": "^5.1", // v5.2.0
        "symfony/asset": "^4.0", // v4.1.4
        "symfony/console": "^4.0", // v4.1.4
        "symfony/flex": "^1.0", // v1.17.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/serializer-pack": "^1.0", // v1.0.1
        "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": {
        "symfony/debug-pack": "^1.0", // v1.0.6
        "symfony/dotenv": "^4.0", // v4.1.4
        "symfony/maker-bundle": "^1.5", // v1.5.0
        "symfony/phpunit-bridge": "^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
    }
}
userVoice