Cara menggunakan php serialization format

Symfony Doctrine ORM pack, Ramsey UUID library, and JMS Serializer - the three tools you need to follow our step-by-step tutorial

An article by Merlin Carter. I write about developer innovation and new technologies at Project A — a venture capital investor focusing on early-stage startups.

Co-authored with Zoltan Kincses, Senior Developer at Project A

Just over a year ago, we published a tutorial titled “ Serializing data in PHP: A simple primer on the JMS Serializer and FoS Rest”. We heard from a few people who found it helpful, so we decided to publish a follow-up.

Both this follow-up and the previous tutorial are aimed at developers who are new to PHP or building APIs in general. We created it to help our own junior developers and we want to share it with you too.

This time, we’ll be focusing on interacting with a database rather than just serializing data. You don’t need to have read the previous tutorial to follow this one, but we will be glossing over some steps that are explained in more detail there.

Here’s a brief recap of what we did in the previous tutorial:

  • We created a REST endpoint that accepted the basic properties of a postal address as a JSON payload.
  • We then used the JMS serializer to serialize the address details into an object and then deserialize the object back into a different format (XML).

Serializing data in PHP: A simple primer on the JMS Serializer and FoS Rest

You want to create a REST API in PHP? Having a hard time finding decent info about serialization? Check out this super…

insights.project-a.com

This time around, we’re going to add the following steps.

  • We’ll write a POST operation to insert the serialized object into a database and give it a unique ID (UUID).
  • We’ll add GET and DELETE operations to retrieve and remove specific database entries based on their UUIDs.

Here are all the major steps we’ll be covering:

  1. Bootstrap a skeleton project with the Symfony CLI.
  2. Install the required dependencies.
  3. Set up a MySQL database.
  4. Create an Address Entity that includes a UUID.
  5. Run Migrations to Create a Table for our Entity.
  6. Create a controller that handles write requests to the database.
  7. Update the controller to handle read and delete requests.

What you’ll need to start

You’ll need the same requisites as the previous tutorial, plus a MySQL database server.

Here’s a full list of all the prerequisites again:

  • An IDE for editing PHP files
    — You can use whatever you like, but to get started quickly, I recommend setting up a trial account with Codeanywhere. They have a PHP container template with PHP, Composer, MySQL, phpMyAdmin, and many other dependencies preinstalled.
    — If you’re using something else, make sure that you have PHP 7.4.16+* and Composer for installing bundles.
    * Naturally, PHP 8.1 is now out with many new features, but since we started this series on 7.x, we’re using 7.4 to be consistent. Version 7.4.16 is also the default version that comes preinstalled on the Codeanywhere PHP container.
  • The Symfony CLI
    — Just like last time, we’ll use the Symfony CLI to bootstrap our project. After you’ve installed it, run
    composer require symfony/orm-pack ramsey/uuid-doctrine
    8to make sure all the required PHP dependencies are installed.
  • A REST API Client
    —You’ll need some kind of client to make API requests. In this walkthrough, we're just going to use plain old cURL (which comes preinstalled on Codeanywhere) but you can also use a UI-based client such as Postman.
  • MySQL server
    — If you using Codeanywhere, this is already installed, but you’re following along on a local IDE, you’ll need to install it yourself (Digital Ocean has some decent installation instructions for Ubuntu).

We’ll also be using some specific PHP bundles but we’ll show you how to install those in the tutorial.

It also helps to have a basic knowledge of object-oriented programming in PHP.

Ok, let’s go!

Setting Up

Let’s bootstrap a new Symfony project and install all the extra bundles we need.

Create an empty PHP Skeleton Project

Use Symfony CLI to create a Symfony skeleton project called

composer require symfony/orm-pack ramsey/uuid-doctrine
9 (or whatever else you want to call it).

Enter the following command:

$ symfony new jms-db-tutorial

It’s going to create the folder structure and initial files that you need for this tutorial.

Install the extra PHP Bundles

  1. First, if you haven’t already, install the JMS serializer:
composer require jms/serializer-bundle

2. Install, the dependencies that we used in the last tutorial.

composer require --dev symfony/maker-bundle
composer require friendsofsymfony/rest-bundle
composer require sensio/framework-extra-bundle

3. Update the FOS Rest config file:

types:
uuid: Ramsey\Uuid\Doctrine\UuidType
0
Make sure that it resembles the following example:

FOSRest config example from the “Handling the View” section of the previous tutorial.

4. Install Symfony’s Doctrine ORM pack and the Doctrine-compatible UUID library from Ben Ramsey.

composer require symfony/orm-pack ramsey/uuid-doctrine

5. Update the configuration for Doctrine’s Database Abstraction Layer (DBAL) and add UUID as a field type.

Open the following file:

types:
uuid: Ramsey\Uuid\Doctrine\UuidType
1

In the

types:
uuid: Ramsey\Uuid\Doctrine\UuidType
2 section, add the following lines:

types:
uuid: Ramsey\Uuid\Doctrine\UuidType

Your updated configuration file should look like this:

Now you’re ready to set up your database.

Preparing the Database

For the sake of simplicity, we’ll be using the

types:
uuid: Ramsey\Uuid\Doctrine\UuidType
3 user for our database transactions. Of course, in a production scenario, you should never use
types:
uuid: Ramsey\Uuid\Doctrine\UuidType
3. Always create and configure a dedicated database user for your application.

However, even when using

types:
uuid: Ramsey\Uuid\Doctrine\UuidType
3, we need to ensure that the root user has a password.

Depending on how you installed MySQL, your root user might not have a password initially. For example, if you’re using Codeanywhere, the root user doesn’t have a password by default.

So, if this applies to you, first…

Set the root user password

To give your root user a password, follow these steps:

  • Start a MySQL shell:
    types:
    uuid: Ramsey\Uuid\Doctrine\UuidType
    6
  • Execute the command:
SET PASSWORD FOR 'root'@'localhost' = PASSWORD('YourNewPassword');
  • Exit the shell and log in again with your password:
    types:
    uuid: Ramsey\Uuid\Doctrine\UuidType
    7(You’ll get a password prompt after you hit Enter)

Create a Database

We’re creating an address entity that is usually attached to a customer record of some sort. So let’s call our database ‘customer’.

In the MySQL shell, execute the following command:

mysql> CREATE DATABASE customer;

Configure the Database Connection

Once your database is set up, configure your Symfony application to connect to your database.

To do configure the database connection, follow these steps:

  1. Open the
    types:
    uuid: Ramsey\Uuid\Doctrine\UuidType
    8 file at the root of your project:
    The default database connection string is usually for a Postgres Database but we want to switch it to MySQL.
  2. Find the line that begins with
    types:
    uuid: Ramsey\Uuid\Doctrine\UuidType
    9 and comment it out with ‘#’ (if it’s not already).
  3. Find the line that begins with
    SET PASSWORD FOR 'root'@'localhost' = PASSWORD('YourNewPassword');
    0 , uncomment it, and update the placeholder values (
    SET PASSWORD FOR 'root'@'localhost' = PASSWORD('YourNewPassword');
    1,
    SET PASSWORD FOR 'root'@'localhost' = PASSWORD('YourNewPassword');
    2, and
    SET PASSWORD FOR 'root'@'localhost' = PASSWORD('YourNewPassword');
    3) with
    types:
    uuid: Ramsey\Uuid\Doctrine\UuidType
    3, your MySQL root user password, and
    SET PASSWORD FOR 'root'@'localhost' = PASSWORD('YourNewPassword');
    5 respectively.
  4. Save your changes.

Your

types:
uuid: Ramsey\Uuid\Doctrine\UuidType
8 should now resemble the following example:

Creating the Address Entity

If you followed the previous tutorial, you might remember creating an address entity already, but let’s do it again from scratch.

This time, we’re going to use the maker bundle which is more efficient.

To create the Address entity, follow these steps:

  1. Open a terminal window in the root of your project and enter the following command:
    SET PASSWORD FOR 'root'@'localhost' = PASSWORD('YourNewPassword');
    7You’ll be prompted to give it a name and define some properties.
  2. Call it
    SET PASSWORD FOR 'root'@'localhost' = PASSWORD('YourNewPassword');
    8and define the same properties that we had in the last tutorial, which were:
    — name
    — line1
    — line2
    — line3
    — city
    — postcode
    — country
  3. For simplicity's sake, accept the defaults for the field attributes by pressing
    SET PASSWORD FOR 'root'@'localhost' = PASSWORD('YourNewPassword');
    9… with one exception:
    Let’s make it so that “line3” can be NULL (many addresses don’t have enough details to warrant a third line).
  4. When you get to “line3”, override the default suggestion and answer ‘yes’ to “can this field be null?”.

Let’s take a look at what was automatically generated for us. Open the new entity file

mysql> CREATE DATABASE customer;
0

The first part of the file should resemble the following example:

This is a good start, but we need to make a lot of changes to this entity. The first thing we need to do is import the required bundles.

Import the UUID and JMS Serializer bundles

Under the existing imports, add the following lines:

use Ramsey\Uuid\Doctrine\UuidGenerator;
use JMS\Serializer\Annotation as Serializer;

The first bundle is for UUID generation. We’re using the UUD bundle from Ben Ramsey because it’s well documented and you can also use it independent of Symfony. The second bundle is our trusty JMS serializer to handle the incoming data. As in the last tutorial, we’ll need to add annotations to configure the serializer, but first…

A short note about the Repository Class Argument

By default, Symfony creates an Entity that’s annotated with a specific Repository class like so:

mysql> CREATE DATABASE customer;
1

However, in this basic tutorial, we’re not going to use . In a production scenario, the official Symfony documentation encourages you to register repositories this way but it’s also valid to omit this argument (so that the entity is not dependent on a specific repository). You can find a full explanation in the article “How to use Repository with Doctrine as Service in Symfony”.

Add the JMS exclusion policy.

If you remember from the last tutorial, we want the JMS serializer to ignore all properties except the ones in our Entity. So we exclude everything by default and then expose the permitted properties explicitly.

To exclude everything by default, add the annotation

mysql> CREATE DATABASE customer;
2underneath the
mysql> CREATE DATABASE customer;
3annotation:

The first part of your Address class should now look like this:

/**
* @ORM\Entity()
* @Serializer\ExclusionPolicy("all")
*/
class Address

Update the ID to be a UUID instead of an integer

You can see that the maker bundle has automatically created an

mysql> CREATE DATABASE customer;
4property as well as the properties we explicitly defined in the wizard.

You can see that it’s configured to generate simple integers for the entry IDs:

/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;

Every entry in a database needs some kind of ID, but incremental IDs are very easy to guess. We want to make sure that mischievous actors can’t access our database entries by trying out different numbers.

Instead, we want to reconfigure the

mysql> CREATE DATABASE customer;
4property so that a true UUID is generated for every new database entry (such as ‘ebb5c735–0308–4e3c-9aea-8a270aebfe15’).

To do so, update the annotation block with the following changes:

composer require jms/serializer-bundle
0

What have we done exactly?

  • The
    mysql> CREATE DATABASE customer;
    6 tag provides a hint to your IDE about the property type which can be useful for autocompletion. In this case, the type is “Uuid” based on the Ramsey UUID interface.
  • We’re also changing the database column type from an integer to a UUID (with
    mysql> CREATE DATABASE customer;
    7)
  • We’re indicating that we’re using a custom method (
    mysql> CREATE DATABASE customer;
    8) to generate the ID (
    mysql> CREATE DATABASE customer;
    9).
  • Then, we’re defining the custom UUID generator that we want to use:
    use Ramsey\Uuid\Doctrine\UuidGenerator;
    use JMS\Serializer\Annotation as Serializer;
    0

Expose properties to override JMS serializer exclusion policy

We need to expose our Address properties for serialization (like we did in the previous tutorial).

For each of the remaining Address properties (starting with “name), update the annotation block to include the relevant JMS serializer annotations.

Here’s an example of how the annotations should look for the name property:

composer require jms/serializer-bundle
1

To recap, here’s how your Address entity should look after you’ve completed all the required changes:

The entity has been truncated for readability.

Creating and Executing the Database Migrations

Congratulations! You’re now ready to create a database migration for your address Entity. Migrations allow you to incrementally change your data structures in a way that’s easy to roll back.

To create migration for your Entity, run the following command in the root of your project:

composer require jms/serializer-bundle
2

Assuming everything worked, you’ll find the result of the migration in the

use Ramsey\Uuid\Doctrine\UuidGenerator;
use JMS\Serializer\Annotation as Serializer;
1 folder. It will be called something like “Version20211222122815.php”

Let’s take a quick look inside:

It contains simple SQL commands to run on the customer database. It will create an “address” table and add columns for each of the properties in the entity.

Note the columns that have special configurations such as the “Id” column and the “Line3” column. The “Line3” is the only column to have “NULL” as its default — the others are not allowed to be NULL.

Assuming everything looks fine, it’s time to execute the migrations on the database.

To do so, run the command:

composer require jms/serializer-bundle
3

The SQL commands are run and the “address” table is created in our “customer” database.

To verify this, let’s query our database. Start a MySQL shell with

use Ramsey\Uuid\Doctrine\UuidGenerator;
use JMS\Serializer\Annotation as Serializer;
2and run the following commands:

composer require jms/serializer-bundle
4

You should see a new address table.

Now, let’s look at the columns by running

composer require jms/serializer-bundle
5

You should see that the

use Ramsey\Uuid\Doctrine\UuidGenerator;
use JMS\Serializer\Annotation as Serializer;
3column is the primary key and that
use Ramsey\Uuid\Doctrine\UuidGenerator;
use JMS\Serializer\Annotation as Serializer;
4is the only column that’s allowed to be NULL:

Now it’s time to set up our API endpoint which will write data to this table.

Create an Address Controller

To create an address controller, we use the same command from the previous tutorial:

composer require jms/serializer-bundle
6

When prompted, call it “Address”.

As before, we get some boilerplate code that we’ll need to change:

Add the required imports

First, import the address entity that we created previously

composer require jms/serializer-bundle
7

Next, import all the extra bundles that we used in the previous tutorial as well as the Doctrine Entity Manager Interface which is a new addition.

composer require jms/serializer-bundle
8

Update the Base Class

Just like in the first tutorial, change the base class from

use Ramsey\Uuid\Doctrine\UuidGenerator;
use JMS\Serializer\Annotation as Serializer;
5 to
use Ramsey\Uuid\Doctrine\UuidGenerator;
use JMS\Serializer\Annotation as Serializer;
6— we want to use the extra features that come with the FOSRest bundle.

The class definition should now look like this:

composer require jms/serializer-bundle
9

Add a property the Entity Manager Interfaces

We want to use the Entity Manager interface to write our data to the database. This type around, we’ll add it using the typed properties that were introduced in PHP 7.4.

Inside the

use Ramsey\Uuid\Doctrine\UuidGenerator;
use JMS\Serializer\Annotation as Serializer;
7class, assign the Entity Manager interface to a property like so:

composer require --dev symfony/maker-bundle
composer require friendsofsymfony/rest-bundle
composer require sensio/framework-extra-bundle
0

Underneath the property definition, add the property to a constructor like so:

composer require --dev symfony/maker-bundle
composer require friendsofsymfony/rest-bundle
composer require sensio/framework-extra-bundle
1

The first part of your controller should now look like this:

Create a POST endpoint

Now it’s time to define an endpoint to receive our data. Replace the boilerplate

use Ramsey\Uuid\Doctrine\UuidGenerator;
use JMS\Serializer\Annotation as Serializer;
8function with an empty
use Ramsey\Uuid\Doctrine\UuidGenerator;
use JMS\Serializer\Annotation as Serializer;
9function.

composer require --dev symfony/maker-bundle
composer require friendsofsymfony/rest-bundle
composer require sensio/framework-extra-bundle
2

Inside the empty function, add the following properties

composer require --dev symfony/maker-bundle
composer require friendsofsymfony/rest-bundle
composer require sensio/framework-extra-bundle
3
  • The
    /**
    * @ORM\Entity()
    * @Serializer\ExclusionPolicy("all")
    */
    class Address
    0 call instructs the Entity Manager to track this change in memory.
  • The
    /**
    * @ORM\Entity()
    * @Serializer\ExclusionPolicy("all")
    */
    class Address
    1call tells the Entity Manager to actually execute this change on the database.

Next, we add the FOSRest

/**
* @ORM\Entity()
* @Serializer\ExclusionPolicy("all")
*/
class Address
2 method from the previous tutorial:

composer require --dev symfony/maker-bundle
composer require friendsofsymfony/rest-bundle
composer require sensio/framework-extra-bundle
4

This demonstrates how to serialize the data into another format such as XML. Our POST request will respond with this serialized version of the JSON payload.

Your

/**
* @ORM\Entity()
* @Serializer\ExclusionPolicy("all")
*/
class Address
3 action should now look like this:

As we’ve noted in the code, you should add validation when using something like this in production. Symfony’s built-in helps you verify data constraints that are defined in an entity class.

Test the endpoint

OK, now it’s time to see if it works. First off, start the Symfony server by running the following command in the project root:

composer require --dev symfony/maker-bundle
composer require friendsofsymfony/rest-bundle
composer require sensio/framework-extra-bundle
5

In a new terminal window, enter the following cURL command:

composer require --dev symfony/maker-bundle
composer require friendsofsymfony/rest-bundle
composer require sensio/framework-extra-bundle
6

Note that the “line3” property has been left empty on purpose. We want to make sure that the database will accept a NULL value in the equivalent column.

You should get the same data serialized back as XML.

This time, we want to verify that the data was also written to the database.

Log into a MySQL shell with

use Ramsey\Uuid\Doctrine\UuidGenerator;
use JMS\Serializer\Annotation as Serializer;
2and run the following commands:

composer require --dev symfony/maker-bundle
composer require friendsofsymfony/rest-bundle
composer require sensio/framework-extra-bundle
7

You should now see that our entry for “Horst Konrader” has made it into the database, complete with UUID.

Now we can use that UUID to retrieve the details for the entry from the database. Make a note of the UUD for the next step.

Create a GET endpoint

Let’s create a new endpoint that accepts a UUID and returns the entry details as XML (with our

/**
* @ORM\Entity()
* @Serializer\ExclusionPolicy("all")
*/
class Address
2method).

To create the GET endpoint, follow these steps:

1. Make a copy of the POST function underneath the existing one and remove the existing properties inside it.

2. Change the

/**
* @ORM\Entity()
* @Serializer\ExclusionPolicy("all")
*/
class Address
6annotation as follows:

composer require --dev symfony/maker-bundle
composer require friendsofsymfony/rest-bundle
composer require sensio/framework-extra-bundle
8

3. Remove the

/**
* @ORM\Entity()
* @Serializer\ExclusionPolicy("all")
*/
class Address
7annotation (since we’re not sending a payload, we have nothing to convert).

4. Change the function name to

/**
* @ORM\Entity()
* @Serializer\ExclusionPolicy("all")
*/
class Address
8and change the function argument from
/**
* @ORM\Entity()
* @Serializer\ExclusionPolicy("all")
*/
class Address
9to
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
0.
This time, we want to work with the UUID which is passed a path segment parameter (rather than the Address payload).

5. Let’s add some error handling in case the UUID doesn’t exist in the database — add the following block inside the function:

composer require --dev symfony/maker-bundle
composer require friendsofsymfony/rest-bundle
composer require sensio/framework-extra-bundle
9

The

/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
1 method is a that returns a 404 status code and an error page when the UUD wasn’t found in the database.

6. If the entry does exist, we want to serialize it into XML and return it as a response — to do so, add the following line.

composer require --dev symfony/maker-bundle
composer require friendsofsymfony/rest-bundle
composer require sensio/framework-extra-bundle
4

The full

/**
* @ORM\Entity()
* @Serializer\ExclusionPolicy("all")
*/
class Address
8action should look like this:

Now let’s try it out.

Send a GET request with the following command but replace the UUID with the one you noted from the previous section.

composer require symfony/orm-pack ramsey/uuid-doctrine
1

You should get the same XML details back as your response.

composer require symfony/orm-pack ramsey/uuid-doctrine
2

Create a DELETE endpoint

Finally, let’s remove the entry with a DELETE endpoint.

To create the DELETE endpoint, follow these steps:

  1. Make a copy of the GET function underneath the existing one.

2. Change the

/**
* @ORM\Entity()
* @Serializer\ExclusionPolicy("all")
*/
class Address
6annotation as follows:

composer require symfony/orm-pack ramsey/uuid-doctrine
3

3. Change function name to

/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
4.

We still need the same error handling that we had for the GET request, so keep that section.

4. Underneath the error handling block, add the following lines:

composer require symfony/orm-pack ramsey/uuid-doctrine
4
  • Again, the
    /**
    * @ORM\Id
    * @ORM\GeneratedValue
    * @ORM\Column(type="integer")
    */
    private $id;
    5 call instructs the Entity Manager to track the removal in memory.
  • The
    /**
    * @ORM\Entity()
    * @Serializer\ExclusionPolicy("all")
    */
    class Address
    1call tells the Entity Manager to actually execute the remove operation on the database and delete the entry.

5. Lastly, we want to return a simple status code if the operation completed successfully — to do so, add the following line:

composer require symfony/orm-pack ramsey/uuid-doctrine
5

Your

/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
4 action should now look like this:

OK, you know the drill. Let’s test it by running a request with the following command:
(As before, replace the sample UUID with the one you noted previously)

composer require symfony/orm-pack ramsey/uuid-doctrine
6

To make sure that it worked, let’s query our database again. Start a MySQL shell with

use Ramsey\Uuid\Doctrine\UuidGenerator;
use JMS\Serializer\Annotation as Serializer;
2and run the following commands:

composer require symfony/orm-pack ramsey/uuid-doctrine
7

The entry with the noted UUID should hopefully be gone.

Wrapping Up

Congratulations for making it this far! If you want to see the entire Address Entity and Controller, take a look at the gists linked below:

To see what else you can do with databases (such as updating or listing entries), check out the Symfony documentation on Databases and the Doctrine ORM.

In tutorials like these, we try to fill in any documentation gaps that we found to be lacking on our own journeys to PHP mastery. And we hope this simple primer helped to bring you a little bit closer to creating a full-fledged PHP application yourself.

If you feel like you’re ready to build this example out even further, try mastering and implementing JWT Authentication.

And as always, if there’s anything else you think isn’t documented very well and could use a decent write-up, let us know!