Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

FosRestBundle PATCH action prevent update entity with null/default values

I have made a working patchAction on my serverController to update one or some field in a existent server.

Actually my patchAction look like this

/*
 * @ParamConverter("updatedServer", converter="fos_rest.request_body")
 *
 * @return View
 */
public function patchAction(Server $server, Server $updatedServer, ConstraintViolationListInterface $validationErrors)
{
    if ($validationErrors->count() > 0) {
        return $this->handleBodyValidationErrorsView($validationErrors);
    }

    $server->setAlias($updatedServer->getAlias())
        ->setMac($updatedServer->getMac())
        ->setSshUser($updatedServer->getSshUser())
        ->setSshPort($updatedServer->getSshPort())
        ->setIpmiAddress($updatedServer->getIpmiAddress())
        ->setIpmiLogin($updatedServer->getIpmiLogin())
        ->setIpmiPassword($updatedServer->getIpmiPassword())
        ->setMysqlHost($updatedServer->getMysqlHost())
        ->setMysqlRoot($updatedServer->getMysqlRoot())
        ->setWebServer($updatedServer->getWebServer())
        ->setWebServerSslListen($updatedServer->getWebServerSslListen())
        ->setWebServerSslPort($updatedServer->getWebServerSslPort())
        ->setMysqlServer($updatedServer->getMysqlServer())
        ->setSuphp($updatedServer->getSuphp())
        ->setFastcgi($updatedServer->getFastcgi())
        ->setNadminCompliant($updatedServer->getNadminCompliant())
        ->setEmailCompliant($updatedServer->getEmailCompliant())
        ->setAvailable($updatedServer->getAvailable())
        ->setEnvironment($updatedServer->getEnvironment())
        ->setInstalledAt($updatedServer->getInstalledAt());

    if (null !== $updatedServer->getOs()) {
        $os = $this->getDoctrine()->getRepository('AppBundle:Os')->findBy(['id' => $updatedServer->getOs()->getId()]);
        $server->setOs($os[0]);
    }

    if (null !== $updatedServer->getPuppetClasses()) {
        $puppetClass = $this->getDoctrine()->getRepository('AppBundle:PuppetClass')->findBy(['id' => $updatedServer->getPuppetClasses()[0]->getId()]);
        $server->setPuppetClasses($puppetClass);
    }

    if (null !== $updatedServer->getPuppetTemplates()) {
        $puppetTemplate = $this->getDoctrine()->getRepository('AppBundle:PuppetTemplate')->findBy(['id' => $updatedServer->getPuppetTemplates()[0]->getId()]);
        $server->setPuppetTemplates($puppetTemplate);
    }

    if (null !== $updatedServer->getBackupModel()) {
        $backupModel = $this->getDoctrine()->getRepository('AppBundle:BackupModel')->findBy(['id' => $updatedServer->getBackupModel()->getId()]);
        $server->setBackupModel($backupModel[0]);
    }

    $em = $this->getDoctrine()->getManager();

    $em->persist($server);
    $em->flush();

    return $this->view([$updatedServer, $server]);
}

The problem is when trying to update only one or few fields. I set a JSON body to change datas.

{
    "mac": "ff:ff:ff:ff:ff:ff"
}

The JSON body will look like this after I send the request

// This is what $updatedServer get in my controller
{
    "id": null,
    "name": null,
    "alias": null,
    "notes": null,
    "hosted_domain": null,
    "mac": "ff:ff:ff:ff:ff:ff",
    // ...
}

As you can see above in my controller I have set every updatable fields

$server->setAlias($updatedServer->getAlias())
    ->setMac($updatedServer->getMac())
    ->setSshUser($updatedServer->getSshUser())
    // ...

So if a value is null inside the body request the controller will set it to null, it will do the same with the default values set inside the entity

I had the idea to make a if condition for every updatable fields but I will have >20 conditions inside...

How can I prevent this to append with a generic and reusable system ?

Can these values can be ignored if they are not set before the execution of the request ?

Maybe create callback inside my entity class ?

Thanks

EDIT

My Server $server is the current server object I want to update. It's coming with the request. Exemple when I send this request /api/servers/2 I get the body of the server with the ID 2.

The Server $updatedServer is the body with the updated datas.

I tried your second edit but I get a 500 error

"Unable to guess how to get a Doctrine instance from the request information."

Because I can't get the server I want to patch and the body (with ParamConverter) in the same time.

like image 582
Jérôme Avatar asked May 17 '26 22:05

Jérôme


1 Answers

You can prevent this by using the PATCH method as the specification explain it (the right way).

As its name sounds, a PATCH method is used to send a patch that updates an existing resource.

To use it correctly, you need to send the whole new state of your resource.
In other words, you must send all the properties of your resource, those unchanged included.

So, if you send each property with its corresponding value, your patch will be correctly applied.

Example:

{
    "id": 1, # Identifier, never change
    "name": [oldValue],
    "alias": [oldValue],
    "notes": [oldValue],
    "hosted_domain": [oldValue],
    "mac": "ff:ff:ff:ff:ff:ff",
    // ...    
}

Like so, no need of write a check per property.

There is a good reference to this common bad usage that is Don't PATCH like an idiot from William Durand.

EDIT

I was wrong.

You don't need to update your resource entirely to use PATCH correctly. You need to send a set of changes, like described in the link given.

The better advice I can give you is to use a ParamConverter that retrieves your object by its identifier and do the update for you depending on fields you give it.

EDIT2

I mean:

/*
 * @ParamConverter("server", converter="fos_rest.request_body")
 *
 * @return View
 */
public function patchAction(Server $server, ConstraintViolationListInterface $validationErrors)
{
    if ($validationErrors->count() > 0) {
        return $this->handleBodyValidationErrorsView($validationErrors);
    }

    $em = $this->getDoctrine()->getManager();

    $em->persist($server);
    $em->flush();
    
    // ...
}

Otherwise, you need an Assembler object used as intermediary step for the merge.
See this great example of handling PATCH requests through FOSRest.

like image 51
chalasr Avatar answered May 19 '26 13:05

chalasr