Efficient Entity Updates with the API Platform’s patch_item Operation

One of the handy features of the API Platform is the ability to use the patch_item operation to update specific fields of an entity, rather than replacing the entire object. This can be a more efficient and flexible way to handle updates in your API.

To use the patch_item operation, you’ll need to make a PATCH request to the relevant endpoint, with the fields you want to update specified in the request body. For example, if you have an entity with fields id, name, and description, and you want to update just the name, your request might look like this:

Copy codePATCH /items/123
{
  "name": "Updated name"
}

This will update the name field for the item with id 123, leaving the other fields unchanged.

You can also use the patch_item operation to update related entities. For example, if you have an Item entity that has a many-to-one relationship with a Category entity, you can use the patch_item operation to update the Category for an Item like this:

Copy codePATCH /items/123
{
  "category": {"id": 456}
}

This will update the Category for the Item with id 123 to the Category with id 456.

Overall, the patch_item operation can be a useful tool for updating specific fields in your API, and can help you build more efficient and flexible APIs with the API Platform.

CapacitorJS

I have been using Cordova or Phonegap build for years. Recently, Phonegap was closed down. On a current project, I had to develop a feature I hadn’t done before. I could find numerous plugins on Cordova site. So…

One by one I start testing. None of them solved the problem. I searched the plugin list, and by either last updated date, or quality ratings, none solved the problem.

I found a “passable solution” although management wasn’t satisfied, the app passed all functional requirements.

It nagged at me though, why could all of the requirements I have not find a plugin less than 3 years old?

In a restless night, I searched YouTube. I came across CapacitorJS. Most of the Tutorials were for Ionic Framework, which is understandable. So I informed management, next release we take time to rebuild it.

Just before my eyes slammed shut from screen exhaustion I caught a glimmer of… hope in documentation. Capacitor can work with ANY front end JS framework. Clock strikes 2 A.M., I text the CTO “3 days we’ll have a demo with capacitor for. you to review”.

The journey began…

The immediate project to app took minutes. But, to solve what kept my eyes from resting, that took time. Not because of a weakness of Capacitor, mind you. Within 7 hours the app was moved over, the UI was better, the code was more concise. Then, a snag. Some features were using Cordova plugins which needed to be replaced. In itself, it would have not taken so long, but I was unable to find docs fitting my project structure.

This was a breath of fresh air: https://github.com/EscolaDeSaudePublica/iSaude

The last elements I could not solve were not covered here, but the way of making them work with VueJS. So, credit where credit is due. This was a perfect VueJS example.

On the road ahead

Now, with that knowledge, I am able to get new features implemented faster. Far less code to manage. I cloned the above repo, I think going forward its going to be a boilerplate.

So, thanks @capacitorJS team!

MacOS Setup for PHP Dev

I wanted a simple, basic Mac OSX setup for PHP dev. With as little as possible manual work.

My current work as you from a previous post is:

  • Symfony 5
  • Api-Platform
  • VueJS front end

Mac OSX 10.15.5 comes with php 7.3.11 out of the box. That was high enough for my initial dev.

So here were my steps:

  1. Install composer getcomposer.org (command line)
  2. install node (npm), used the pkg from their website
  3. added yarn (command line)
  4. Installed Docker (from website)
  5. installed symfony (command line)

Done, I was now up and running. My previous mac had homebrew setup, with higher PHP version, so copying my projects over didn’t work immediately, I had to delete vendor folder and composer.lock and run composer install

DONE!

Mercure with Apache

One of the things which I absolutely love about ApiPlatform is its deep integration with Mercure and all the real time stuff which just works.

My issue this week was, the server was Apache… and I could not just copy/paste from the docs.

Here is what worked as our final setup:

JWT_KEY='!ChangeMe!' CORS_ALLOWED_ORIGINS=* ADDR='localhost:3000' /home/mercure/mercure

and for apache the following conf:

<VirtualHost *:443>
    ServerName my_dmain_in_dns
    ServerAlias my_dmain_in_dns
    ProxyRequests Off
 ### your SSL key info here
 <LocationMatch /hub >
       ProxyPass http://localhost:3000
       ProxyPassReverse http://localhost:3000
 </LocationMatch>
 </VirtualHost>

Keep in mind, now your client will connect to my_domain_in_dns/hub and it will be routed to localhost:3000

Then of course setup supervisor and good to go!

ApiPlatform and my security go-tos

There are 4 security go-tos I use in my ApiPlatform projects. I’ll describe them below

Use only needed endpoints

Entities will automatically make all the usual endpoints. But sometimes thats a bit too much. Take a look at https://symfonycasts.com/screencast/api-platform/operations and see how you can get rid of the points you won’t use. Don’t want to ever delete from a DB? Take it out from your item operations, now if someone tries system will stop it.

is_granted

I heavily use the is_granted method. In fact, although I know its from Symfony, it was reading that in ApiPlatform docs that got me the first time. Its a way of quickly visualizing for me what I allow and for who. Yes, its not enough, but its a start.

* collectionOperations={
*         "get"={"security"="is_granted('ROLE_ADMIN')"}
* }

object.

You can use the “object” to match something to the current model. For example:

object.getId()==user.getTeam().getId()

The above on Team entity will make sure the logged in user has the same team ID as the team I am trying to edit/view or wherever I have that check.

Controller

Sometimes you need a bit more logic. For example, although I am sure there is a more elegant way, I had not found a better way to automatically inject the Team to my entity. So… I use a custom controller on a “regular” endpoint. This involves the annotation in collectionOperations:

*          "post"={
*               "method"="GET",
*              "controller"=DashboardGetCollectionController::class,
*          }

And the controller itself. These controllers must have an __invoke method. Here is a sample

public function __invoke(UserInterface $user, EntityManagerInterface $em): array
{
if ($user instanceof User) {
return $em->getRepository(Dashboard::class)->findBy(['owner' => $user]);
}

return [];
}

Although I use a similar method for other ways of scoping data…

Api-platform and Mercure

This was an interesting one for me. It took me quite a while to clearly get the private subscriptions. Here is what it was in the end:

AuthenticationSuccessListener

    public function onAuthenticationSuccessResponse(AuthenticationSuccessEvent $event): void
    {
        $data = $event->getData();
        $user = $event->getUser();

        if (!$user instanceof User) {
            return;
        }

        $token = (new Builder())
            ->withClaim('mercure', ['subscribe' => $user->getMercureIri()])
            ->sign(new Sha256(), 'my_secret_key')
            ->getToken();

        $data['mercureToken'] = $token->__toString();

        $event->setData($data);
    }

User Entity

public function getMercureIri(): array
{
return ['api/users/'.$this->getId()];
}

Remember you need to register the listener in your services.yml but thats about it! Now when you login, you get a Mercure Token which you save, and when you try to subscribe from front end, pass that token… and there it is.

NOTE:

I wasted a few hours because I forgot to destroy the old token on logout, so other users were using the token. Don’t forget to destroy token 🙂

Normalization and Denormalization in ApiPlatform

I have read the docs over at api-platform.com a few times about Normalization and Denormalization. And it just didn’t click. I didn’t see why I would ever need it.

Now, recently, my day job decided we wanted a “central data store” which would serve multiple businesses and multiple interfaces. We found the ideal solution was Normalization groups even on controllers, where we can specify what data for a specific group. Of course that in itself doesn’t do that, be we couple it with Symfony’s security. Only certain groups have access to the “global” api’s, the base entity, or the /api route, however you like to call it.

Then we put business logic in custom controllers, and match those to routes and groups for a specific need. The result was pretty awesome. We manage a datastore which we have never made public before. Now we can control the data, depth of data, and who gets what by combining the Symfony user roles and API Platform normalization.

Well, I don’t know if I can explain it well, but as far as the code… it was a lightbulb moment.

VueJS computed elements

Computed elements, a noble idea I thought I had no use for. I do most my logic in methods. So, when I needed to, a method can modify the variable I would need on the page…

and then…

Today I was working on a stripe.com payment module, and randomly the payment form would not load. I needed to show the form conditionally if an element in the backend database was true. Well, that would hide the div.

What was the solution? The div became a v-if on a computed element. The computed element would check the API. So, now the page would load before computing, and that solved that.

Now that I wrapped my head around it, I think this would be better in many parts in my apps, and improve speed overall.

Using RabbitMQ for async

In a previous post I spoke about using docker for setting up my dev environment. The Symfony messenger component (although IMO harder to implement that Laravel), was much easier following the flow in the book from @fabpot

First install Messenger

composer require messenger

Make your Message class. Usually this is a class in src/Message and all it does it serialize the object. The the example from @fabpot’s book, a message is put into 2 variables, so in the controller its called as:

            $context = [
                'user_ip' => $request->getClientIp(),
                'user_agent' =>$request->headers->get('user-agent'),
                'referrer'=>$request->headers->get('referer'),
                'permalink'=>$request->getUri(),
            ];
//          
            $this->bus->dispatch(new CommentMessage($comment->getId(), $context));

Now, the CommentMessage simply assigned those to an object. For simplicity, lets follow his example, and I will copy the code in the next few steps:

private $id;
private $context;
public function __construct(int $id, array $context = []) {
$this->id = $id;
$this->context = $context;

}
public function getId(): int
{
return $this->id;
}
public function getContext(): array
{
return $this->context;
}

The logic happens in a MessageHandle, which calls an invoke, such as:

public function __invoke(CommentMessage $message)

At this point configure messenger:

framework:
    messenger:
        transports:
            async: '%env(RABBITMQ_DSN)%'
        routing:
            App\Message\YourMessageClass: async

And add RabbitMQ to your docker

rabbitmq:
image: rabbitmq:3.7-management
ports: [5672, 15672]

You are done, so just run messenger:

symfony console messenger:consume async -vv

Tool Set (part 2)

This is a continuation of the part one post found here

My IDE

Since I build all my new projects in SPA style with Symfony/api-platform backend, I find it helpful to separate my IDEs. I guess this is not ideal, because each IDE has its own style shortcuts… but I have my PHP workflow in PHPStorm, and my Javascript/HTML workflow in VS Code

In the previous part we launched our app with the symfony binary

symfony serve

Now our API will be listening on http://127.0.0.1:8000

Installing maker is a nice easy way to build your entities, and also immediately expose the API.

composer require maker --dev

I wish there was a nice JWT login scaffold available for API platform, but there isn’t. So, my next step is to create the user in Symfony

symfony console make:user

Then make a controller for managing login and registraion. This would be a normal Controller responding in JSON.

Start by making the controller

symfony console make:controller UserController

I use something like the following in mine

<?php

namespace App\Controller;

use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;

class SecurityController extends AbstractController
{
    /**
     * @Route("/login", name="app_login", methods={"POST"})
     */
    public function login()
    {
        return $this->json([
                'user' => $this->getUser() ? $this->getUser()->getId() : null]
        );
    }

    /**
     * @Route("/register", name="app_register", methods={"POST"})
     */
    public function register(Request $request,UserPasswordEncoderInterface $encoder, EntityManagerInterface $entityManager,ValidatorInterface $validator)
    {
        $user = new User();
        $user->setEmail($data['email']);
        $user->setPassword($encoder->encodePassword($user, $data['plainPassword']));

        $entityManager->persist($user);
        $entityManager->flush();
        return new JsonResponse($user);
    }


}

The you’ll need to install the bundle for managing JWT. The instructions are a bit detailed, so get it from the source

Your API is almost ready to go, just make a entity to automatically expose the API.

symfony console make:entity

Now you are well on your way, register, login, and an exposed entity.

You can view your API at https://127.0.0.1:8000/api/docs and if you password protext it, use a tool like postman to login and get a token, so you can authorize and test your API via the docs.

Then you need a front end…