Using Enum in Symfony

Hantsy on 2022-02-27

PHP 8.1 introduces the official Enum support. Doctrine brought Enum type support in its ORM framework, and Symfony added serialization and deserialization support of a Enum type.

Photo by te chan on Unsplash

It is time to migrate your projects to use PHP Enum if you are using 3rd-party enumeration solutions.

To use PHP Enum, you have to upgrade to PHP 8.1, and set the PHP version to 8.1 in the project composer file.

{
    //...
    "require": {
        "php": ">=8.1",
        //...
    }
}

Creating Enum Class

For example, we will add a Status to the Post entity, and defined several fixed values of the post status.

<?php
namespace App\Entity;
enum Status: string
{
    case Draft = "DRAFT";
    case PendingModerated = "PENDING_MODERATED";
    case Published = "PUBLISHED";
}

Here we use a string backed enum, add a field in the Post class.

#[Column(type: "string", enumType: Status::class)]
private Status $status;

Note, set the enumType as the Status class. It will store the status value as a string in the database tables.

In the Post constructor, assign a default value to the status.

public function __construct()
{
    $this->status = Status::Draft;
    //...
}

Now everything is ok.

Creating HttpMethod

When we setup the Route attribute on the Controller class, we use a literal value to set up the HTTP method.

#[Route(path: "/{id}", name: "byId", methods: ["GET"])]

For the methods value, there are only several options available to choose. Obviously, if introducing Enum, it will provide a type-safe way to setup the values and decrease the typo errors.

Create an Enum named HttpMethod.

<?php
namespace App\Annotation;
enum HttpMethod
{
    case GET;
    case POST;
    case HEAD;
    case OPTIONS;
    case PATCH;
    case PUT;
    case DELETE;
}

Then refactor the Route attribute and create a series of attributes(Get, Post, Put, Delete, etc.) that are mapped to different HTTP methods.

//file : src/Annotation/Get.php
#[Attribute]
class Get extends Route
{
    public function getMethods()
    {
        return [HttpMethod::GET->name];
    }
}
//file : src/Annotation/Head.php
#[Attribute]
class Head extends Route
{
    public function getMethods()
    {
        return [HttpMethod::HEAD->name];
    }
}
//file : src/Annotation/Options.php
#[Attribute]
class Options extends Route
{
    public function getMethods()
    {
        return [HttpMethod::OPTIONS->name];
    }
}
//file : src/Annotation/Patch.php
#[Attribute]
class Patch extends Route
{
    public function getMethods()
    {
        return [HttpMethod::PATCH->name];
    }
}
//file : src/Annotation/Post.php
#[Attribute]
class Post extends Route
{
    public function getMethods()
    {
        return [HttpMethod::POST->name];
    }
}
//file : src/Annotation/Put.php
#[Attribute]
class Put extends Route
{
    public function getMethods()
    {
        return [HttpMethod::PUT->name];
    }
}
//file : src/Annotation/Delete.php
#[Attribute]
class Delete extends Route
{
    public function getMethods()
    {
        return [HttpMethod::DELETE->name];
    }
}

Now you can polish the PostController, use these attributes instead. As you see, the naming of the new attributes literally look more clear.

#[Route(path: "/posts", name: "posts_")]
class PostController extends AbstractController
{
    // constructor...
    // #[Route(path: "", name: "all", methods: ["GET"])]
    #[Get(path: "", name: "all")]
    public function all(#[QueryParam] string $keyword,
                        #[QueryParam] int $offset = 0,
                        #[QueryParam] int $limit = 20): Response
    {
        //...
    }
    // #[Route(path: "/{id}", name: "byId", methods: ["GET"])]
    #[Get(path: "/{id}", name: "byId")]
    public function getById(Uuid $id): Response
    {
       //...
    }
    //#[Route(path: "", name: "create", methods: ["POST"])]
    #[Post(path: "", name: "create")]
    public function create(#[Body] CreatePostDto $data): Response
    {
        //...
    }
    //#[Route(path: "/{id}", name: "update", methods: ["PUT"])]
    #[Put(path: "/{id}", name: "update")]
    public function update(Uuid $id, #[Body] UpdatePostDto $data): Response
    {
        //...
    }
    // #[Route(path: "/{id}/status", name: "update_status", methods: ["PUT"])]
    #[Put(path: "/{id}/status", name: "update_status")]
    public function updateStatus(Uuid $id, #[Body] UpdatePostStatusDto $data): Response
    {
       //...
    }
    //#[Route(path: "/{id}", name: "delete", methods: ["DELETE"])]
    #[Delete(path: "/{id}", name: "delete")]
    public function deleteById(Uuid $id): Response
    {
        //...
    }
    // comments sub resources.
    //#[Route(path: "/{id}/comments", name: "commentByPostId", methods: ["GET"])]
    #[GET(path: "/{id}/comments", name: "commentByPostId")]
    public function getComments(Uuid $id): Response
    {
      //...
    }
    //#[Route(path: "/{id}/comments", name: "addComments", methods: ["POST"])]
    #[Post(path: "/{id}/comments", name: "addComments")]
    public function addComment(Uuid $id, Request $request): Response
    {
		//...
    }
}

Run the application again to make sure it works.

Get the source codes from my Github.

BTW, I have updated my original Symfony posts into a step-by-step guide, read it online: https://hantsy.github.io/symfony-rest-sample/