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 SubscribeLet's add a bonus feature to our app. Right now, the id of each resource is its auto-increment database ID. We can see this on all of our endpoints. If you Execute the cheese collection endpoint... the IRIs are /api/cheeses/1
. You'll also use the database id to update or do anything else.
Using auto-increment id's like this is fine. But it can have a few downsides. For example, it can expose some info - like how many users you have... or - by just changing the ids to 1, 2 or 3, you could easily browse through all of the users... though you should - ya know - use security to avoid this if it's a problem.
Auto-increment IDs have another downside: when you use an auto-increment database id as the key in your API, it means that only your server can create them. But if your API clients - like JavaScript - could instead choose their own id, it would actually simplify their lives. Think about it: if you're creating a new resource in JavaScript, you normally need to send the AJAX call and wait for the response so that you can then use the new id:
const userData = {
// ...
};
axios.post('/api/users', userData).then((response) => {
// response.data contains the user data WITH the id
this.users.push(response.data);
});
That's especially common with frontend frameworks when managing state.
Another option is to use a UUID: a, sort of, randomly-generated string that anyone - including JavaScript - can create. If we allowed that, our JavaScript could generate that UUID, send the AJAX request with that UUID and then not have to wait for the response to update the state:
import { v4 as uuidv4 } from 'uuid';
const userData = {
uuid: uuidv4(),
// ... all the other fields
};
this.users.push(userData);
axios.post('/api/users', userData);
So that's our next mission: replace the auto-increment id with a UUID in our API so that API clients have the option to generate the id themselves. We'll do this for our User
resource.
So... how do we generate these UUID strings? Symfony 5.2 will come with a new UUID component, which should allow us to easily generate UUID's and store them in Doctrine. But since that hasn't been released yet, we can use ramsey/uuid
, which is honestly awesome and has been around for a long time. Also, Ben Ramsey is a really nice dude and an old friend from Nashville. Ben generates the best UUIDs.
Find your terminal and run:
composer require ramsey/uuid-doctrine
The actual library that generate UUID's is ramsey/uuid
. The library we're installing requires that, but also adds a new UUID doctrine type.
This will execute a recipe from the contrib repository so make sure you say "yes" to that or yes permanently. I committed before I ran composer require
, so we can see the changes with:
git status
Ok: it modified the normal files, but also added a configuration file. Let's go check that out: config/packages/ramsey_uuid_doctrine.yaml
:
doctrine: | |
dbal: | |
types: | |
uuid: 'Ramsey\Uuid\Doctrine\UuidType' |
Ah! This adds the new UUID Doctrine type I was talking about. What this allows us to do - back in User
- is add a new property - private $uuid
- and, above this, say @ORM\Column()
with type="uuid"
. That would not have worked before we installed that library and got the new config file. Also set this to unique=true
:
... lines 1 - 42 | |
class User implements UserInterface | |
{ | |
... lines 45 - 51 | |
/** | |
* @ORM\Column(type="uuid", unique=true) | |
*/ | |
private $uuid; | |
... lines 56 - 294 | |
} |
UUID's are strings, but the uuid
type will make sure to store the UUID in the best possible way for whatever database system you're using. And when we query, it will turn that string back into a UUID object, which is ultimately what's stored on this property. You'll see that in a minute.
Could we know remove the auto-increment column and make this the primary key in Doctrine?
... lines 1 - 42 | |
class User implements UserInterface | |
{ | |
/** | |
* @ORM\Id() | |
* @ORM\GeneratedValue() | |
* @ORM\Column(type="integer") | |
*/ | |
private $id; | |
... lines 51 - 294 | |
} |
Yes, you could. But I won't. Why? Some databases - like MySQL - have performance problems with foreign keys when your primary key is a string. PostgreSQL does not have this problem, so do whatever is best for you. But there's no real disadvantage to having the auto-increment primary key, but a UUID as your API identifier.
Ok: we added a new property. So let's generate the migration for it. Find your terminal and run:
symfony console make:migration
Let's go check that out. Go into the migrations/
directory... and open the latest file:
... lines 1 - 2 | |
declare(strict_types=1); | |
namespace DoctrineMigrations; | |
use Doctrine\DBAL\Schema\Schema; | |
use Doctrine\Migrations\AbstractMigration; | |
/** | |
* Auto-generated Migration: Please modify to your needs! | |
*/ | |
final class Version20200909145236 extends AbstractMigration | |
{ | |
public function getDescription() : string | |
{ | |
return ''; | |
} | |
public function up(Schema $schema) : void | |
{ | |
// this up() migration is auto-generated, please modify it to your needs | |
$this->addSql('ALTER TABLE user ADD uuid CHAR(36) NOT NULL COMMENT \'(DC2Type:uuid)\''); | |
$this->addSql('CREATE UNIQUE INDEX UNIQ_8D93D649D17F50A6 ON user (uuid)'); | |
} | |
public function down(Schema $schema) : void | |
{ | |
// this down() migration is auto-generated, please modify it to your needs | |
$this->addSql('DROP INDEX UNIQ_8D93D649D17F50A6 ON user'); | |
$this->addSql('ALTER TABLE user DROP uuid'); | |
} | |
} |
Since I'm using MySQL, you can see that it's storing this as a char
field with a length of 36:
... lines 1 - 12 | |
final class Version20200909145236 extends AbstractMigration | |
{ | |
... lines 15 - 19 | |
public function up(Schema $schema) : void | |
{ | |
... line 22 | |
$this->addSql('ALTER TABLE user ADD uuid CHAR(36) NOT NULL COMMENT \'(DC2Type:uuid)\''); | |
... line 24 | |
} | |
... lines 26 - 32 | |
} |
The only tricky thing is that because we do already have a database with users in it - the fact that this column is NOT NULL
will make the migration fail because the existing records will have no value.
To fix this, temporarily change it to DEFAULT NULL
:
... lines 1 - 12 | |
final class Version20200909145236 extends AbstractMigration | |
{ | |
... lines 15 - 19 | |
public function up(Schema $schema) : void | |
{ | |
... line 22 | |
$this->addSql('ALTER TABLE user ADD uuid CHAR(36) DEFAULT NULL COMMENT \'(DC2Type:uuid)\''); | |
... lines 24 - 25 | |
} | |
... lines 27 - 33 | |
} |
Then, right after this, say $this->addSql()
with UPDATE user SET uuid = UUID()
:
... lines 1 - 12 | |
final class Version20200909145236 extends AbstractMigration | |
{ | |
... lines 15 - 19 | |
public function up(Schema $schema) : void | |
{ | |
... line 22 | |
$this->addSql('ALTER TABLE user ADD uuid CHAR(36) DEFAULT NULL COMMENT \'(DC2Type:uuid)\''); | |
$this->addSql('UPDATE user SET uuid = UUID()'); | |
... line 25 | |
} | |
... lines 27 - 33 | |
} |
That's a MySQL function to generate UUID's.
Let's try this! Back at your terminal, run the migration:
symfony console doctrine:migrations:migrate
It works! Now generate one more migration as a lazy way to set the field back to NOT NULL
:
symfony console make:migration
If you look at the new migration:
... lines 1 - 2 | |
declare(strict_types=1); | |
namespace DoctrineMigrations; | |
use Doctrine\DBAL\Schema\Schema; | |
use Doctrine\Migrations\AbstractMigration; | |
/** | |
* Auto-generated Migration: Please modify to your needs! | |
*/ | |
final class Version20200909145840 extends AbstractMigration | |
{ | |
public function getDescription() : string | |
{ | |
return ''; | |
} | |
public function up(Schema $schema) : void | |
{ | |
// this up() migration is auto-generated, please modify it to your needs | |
$this->addSql('ALTER TABLE user CHANGE uuid uuid CHAR(36) NOT NULL COMMENT \'(DC2Type:uuid)\''); | |
} | |
public function down(Schema $schema) : void | |
{ | |
// this down() migration is auto-generated, please modify it to your needs | |
$this->addSql('ALTER TABLE user CHANGE uuid uuid CHAR(36) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci` COMMENT \'(DC2Type:uuid)\''); | |
} | |
} |
Perfect! This changes uuid
from DEFAULT NULL
to NOT NULL
:
... lines 1 - 12 | |
final class Version20200909145840 extends AbstractMigration | |
{ | |
... lines 15 - 19 | |
public function up(Schema $schema) : void | |
{ | |
... line 22 | |
$this->addSql('ALTER TABLE user CHANGE uuid uuid CHAR(36) NOT NULL COMMENT \'(DC2Type:uuid)\''); | |
} | |
... lines 25 - 30 | |
} |
Run the migrations one last time:
symfony console doctrine:migrations:migrate
Got it! So at this point, we have a new column... but nobody is using it. Let's run the user tests:
symfony php bin/phpunit tests/Functional/UserResourceTest.php
And... ah! It explodes like crazy! Of course:
Column uuid cannot be null.
It's required in the database... but we're never setting it.
Unlike an auto-increment ID, the UUID is not automatically set, which is fine. We can set it ourselves in the constructor. Scroll down... we already have a constructor. Add $this->uuid = Uuid
- auto-complete the one from Ramsey\
- then call uuid4()
, which is how you get a random UUID string:
... lines 1 - 12 | |
use Ramsey\Uuid\Uuid; | |
... lines 14 - 43 | |
class User implements UserInterface | |
{ | |
... lines 46 - 118 | |
public function __construct() | |
{ | |
... line 121 | |
$this->uuid = Uuid::uuid4(); | |
} | |
... lines 124 - 296 | |
} |
Run the tests again:
symfony php bin/phpunit tests/Functional/UserResourceTest.php
Now they're happy!
Next, the UUID is not part of our API at all yet. Let's tell API Platform to start using it as the identifier.
Hey Stéphane,
I just downloaded a fresh version of this course code and was able to execute migrations successfully in both start/ and finish/ folders. Did you download the course code and started from start/ directory? If so, it sounds like you have a bad migration, please, double-check the migration you created.
And good catch on "doctrine:schema:create" command - if it's a project for learning purposes and you don't care about real data in the DB - that's a totally valid way. For production, you may want to fix your migration otherwise you will lose real data.
Cheers!
Why not use UUID and ULID from Symfony https://symfony.com/blog/ne... ?
And what is the difference between uuids and ulids and which one you encourage to go for? I started a new project and I am using ulids, I havent found alot of docs online.
Hey Zack F. !
> Why not use UUID and ULID from Symfony https://symfony.com/blog/ne... ?
Excellent question! Simply because this tutorial predated that feature... by about 2 months :). It was actually introduced already by the time we recorded this (so I knew about it), but it wasn't released stable yet. I mention this here - https://symfonycasts.com/sc... - but overall, it shouldn't make any difference, except that (maybe) using the Symfony UUID component will be even easier to integrate (though using ramsey is also pretty easy).
> And what is the difference between uuids and ulids and which one you encourage to go for
To be honest, I don't know much about these. But from some quick looking, it seems to me like, in practice, ULIDs are... a more awesome version of UUIDs. A ULID might be less random (as they have the created time encoded + are sortable)... but, unless the "created time" is sensitive to be shared publicly, that's kind of an awesome feature! And so is "sortability" - it's kind of nice to be able to sort database records by ULID and have them be in order. So honestly... yea... go for ULIDs! they're also prettier as URLs ;). I don't see any real disadvantage to them.
Cheers!
Hi, I´ve been trying to use the new Symfony UID, but having trouble on the Doctrine Type as the doctrine migration tries to create an BINARY(16) column instead of CHAR(36) as on the video. I´m using MySQL.
Is there a way to update symfony uuid type to use char(36) or it is better to just use the Ramsey as in the course.
Hey Erico Vasconcelos!
I don't know too much about this, but the Symfony version is definitely made to use a REAL UUID type (if your database supports it, which I believe MySQL does not) else it ALWAYS uses binary. Binary is more of a pain to deal with, but it's more efficient, which is why Symfony chose to only offer that. So if you want to use MySQL and want the string representation, then stick with ramsey :). But know that there could be some performance issues (these are often overblown, but it could be a real problem).
Cheers!
Hi Ryan,
For the benefit of creating uuid from client side, I cannot actually see that since most of the time the client still need to wait for the API response to ensure the resource actually is created before they can do some extra jobs. Can you pleas give me some real world examples?
Thanks!
Hey Tuan Vu !
Very good question. And, I think this "advantage" is less common than I originally thought, but it does come in handy sometimes. Here's an example. Suppose you have a list of... products on a page. At the bottom of the list, you have an "add new product" button. When you click that, it instantly adds a new row to the table that renders the new product data (probably with blank or dummy values to start). It also "rolls out" a form on the sidebar with the product fields. As you type into those fields, the new row at the bottom of the table fills in with the new data automatically.
In this situation, if you're using something like React or Vue, you likely have some "products" state/data, which you're looping over to create the rows in the table. When you click "add new product", in order to add the new row immediately (even before saving), you'll add a new item to the "products" data. That will new item will need to have a unique key. You *could* create a fake key, then update it after you save (and get the new id), but it's not as smooth and it would actually cause Vue/React to re-render that row like new item.
So, in short, you need to have a situation where you're already rendering an item even *before* it's saved. It's not the normal situation, but hopefully that answers your question :).
Cheers!
// composer.json
{
"require": {
"php": ">=8.1",
"ext-ctype": "*",
"ext-iconv": "*",
"api-platform/core": "^2.1", // v2.5.10
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"doctrine/annotations": "^1.0", // 1.12.1
"doctrine/doctrine-bundle": "^2.0", // 2.1.2
"doctrine/doctrine-migrations-bundle": "^3.0", // 3.0.2
"doctrine/orm": "^2.4.5", // 2.8.2
"nelmio/cors-bundle": "^2.1", // 2.1.0
"nesbot/carbon": "^2.17", // 2.39.1
"phpdocumentor/reflection-docblock": "^3.0 || ^4.0 || ^5.0", // 5.2.2
"ramsey/uuid-doctrine": "^1.6", // 1.6.0
"symfony/asset": "5.1.*", // v5.1.5
"symfony/console": "5.1.*", // v5.1.5
"symfony/debug-bundle": "5.1.*", // v5.1.5
"symfony/dotenv": "5.1.*", // v5.1.5
"symfony/expression-language": "5.1.*", // v5.1.5
"symfony/flex": "^1.1", // v1.18.7
"symfony/framework-bundle": "5.1.*", // v5.1.5
"symfony/http-client": "5.1.*", // v5.1.5
"symfony/monolog-bundle": "^3.4", // v3.5.0
"symfony/security-bundle": "5.1.*", // v5.1.5
"symfony/twig-bundle": "5.1.*", // v5.1.5
"symfony/validator": "5.1.*", // v5.1.5
"symfony/webpack-encore-bundle": "^1.6", // v1.8.0
"symfony/yaml": "5.1.*" // v5.1.5
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.3", // 3.3.2
"symfony/browser-kit": "5.1.*", // v5.1.5
"symfony/css-selector": "5.1.*", // v5.1.5
"symfony/maker-bundle": "^1.11", // v1.23.0
"symfony/phpunit-bridge": "5.1.*", // v5.1.5
"symfony/stopwatch": "5.1.*", // v5.1.5
"symfony/twig-bundle": "5.1.*", // v5.1.5
"symfony/web-profiler-bundle": "5.1.*", // v5.1.5
"zenstruck/foundry": "^1.1" // v1.8.0
}
}
Hello there
When I run symfony console doctrine:migrations:migrate I have an error with this message " There is no active transaction" (In Connection.php line 1761:)
I use MySql 5.7 with docker-compose
I use the command : symfony console doctrine:schema:create and it's working.