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…

Tool Set (Part 1)

I start all my projects as a fresh Symfony 5 project, using the Symfony installer found at: https://symfony.com/download

symfony new my_project_name

Assuming you have the Symfony installer, thats enough. Symfony installer by default installs just a barebones application with nothing.

I was never a fan of using a full docker setup. But, after reading the book by @fabpot I started to like the idea of having the tools in docker, but the app served by itself. How, my default docker-compose.yml file starts as this

version: '3'
services:
  database:
    image: postgres:11-alpine
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRESDB: db_name
    ports: [5432:5432]

Note the 5432:5432 this allows me to connect to it from my local system, using a tool like psequel if I want. His book also shows how to use the same method for other tools like Mercure or Redis.

Then I start my symfony application in my IDE as follows:

symfony serve

Next I install api-platform

composer require api

and thats when the fun starts…

To be continued…