Voila! Symfony and PHP 8.1

Mert Simsek
Beyn Technology
Published in
9 min readJan 28, 2022

I don’t know where to start but I was excited to write this post. I haven’t been developing applications with PHP and Symfony for a few years. Luckily I’ve built my latest API with Symfony 6 and PHP 8 and I feel like I’m back home. You can understand my feelings as someone who shared this tweet a few years ago.

In this article, I will try to cover how I use PHP 8 and 8.1 additions and Symfony 6 in the web API application I developed its innovations, and structure. Let’s start with PHP 8.1 to dive into it.

Attributes

The first thing I noticed as I read the docs was the attributes feature. Attributes, also known as Annotations in other languages, allow adding metadata without having to parse outdated doc blocks. In the past, PHP only allowed string-type comment blocks. To overcome this problem, the PHP community has developed parsing methods in comments. The PHP 8.0 Attribute feature has brought a natural solution to this problem.

-- Old versions --/**
* @Route("/api/products")
*/
class ProductController
{
/**
* @Route("/{id}", methods={"GET"})
*/
public function list($id) { /* code block */ }
}

Let’s check out the new way with the attributes. Attributes and Annotations are used for the same purpose. “Annotations” is already widely used in PHP libraries and frameworks, plus the name Attributes helps to minimize the variation with Annotations. It’s shiny, isn’t it?

-- PHP 8 --#[Route("/api/products")]
class ProductController
{
#[Route("/{id}", methods: ["GET"])]
public function list($id) { /* code block */ }
}

JIT (Just in Time)

There is a ton of words to talk about this. I plan to write another article regarding it but let me cover it shortly now. PHP 8 will be able to compile the app’s code directly into machine code that the CPU understands without needing the help of an interpretation layer. For CPU-intensive tasks, having a JIT compiler in PHP boasts significant performance gains. I suppose the biggest and most exciting improvement of PHP 8 is the JIT compiler. PHP is an interpreted language, which means it runs in real-time, rather than being compiled and run at launch. This gives us compiled code of PHP, and with it, better performance. By the way, step one in getting JIT compiling turned on for your project is making sure the OPcache extension is installed and enabled.

Constructor Property Promotion

It is one of the features that will make our life easier and enable both less and more understandable code. It drastically reduces a ton of boilerplate code while constructing simple objects or fields. This addition lets us combine class fields, constructor definition, and variable assignments, all in one syntax, into the constructor parameter list. That’s brilliant!

We may imagine that instead of specifying class fields and a constructor, we may combine all of them using constructor property promotion. I got used to referring to it this way from now on.

-- Old versions --class ProductController
{
private ProductService $productService
public function __constructor(ProductService $productService){
$this->productService = $productService;
}
public function list(){ $products = $this->productService->findAll();
}
}

I directly set that value in the method signature and use it everywhere with $this keyword in the same way.

-- PHP 8 --class ProductController
{
public function __constructor(private ProductService $productService)
{
}
public function list(){ $products = $this->productService->findAll();
}
}

Match

The new match statement, which is similar to the switch statement in terms of structure, took its place in version 8. With this new match, the result can be assigned to a variable or returned. This matching feature supports single-line expressions and does not require a break command. I really want to use match expression as much as I can do.

-- Old versions --switch ($method) {
case 'GET':
$this->list();
break;
case 'POST':
$this->create();
break;
}

The new expression is brilliant!

-- PHP 8 --match ($method) {
'GET' => $this->list(),
'POST' => $this->post(),
};

Enums

Enum is the short form for an enumeration type, we use it to group named values. Enums are used instead of strings to denote the status of objects in an organized way. The general syntax will be like this. It’s pretty familiar.

enum OperationEnum: string
{
case BALANCE = "balance";
case DEPOSIT = "deposit";
case WITHDRAW = "withdraw";
case ROLLBACK = "rollback";
}
-- usage --//Let's say we have an Operation class
$operation = new Operation(OperationEnum::DEPOSIT->value);

Enums can be defined just like method structures in a class. This is a very powerful feature, especially when used with the match operator.

enum OperationEnum: string
{
case BALANCE = "balance";
case DEPOSIT = "deposit";
case WITHDRAW = "withdraw";
case ROLLBACK = "rollback";
}
public function getCode(): int
{
return match($this)
{
self::BALANCE => 1,
self::PUBLISHED => 2,
self::WITHDRAW => 3,
self::ROLLBACK => 4
};
}

Symfony 6

The big news of Symfony 6 is that PHP 8.0 is now the minimum required version. The code of Symfony 6 has been updated. You can take advantage of all the new features in PHP. For example, the code includes PHP 8 attributes, more expressive and rigorous type declarations, etc. Basically, there is no big difference between Symfony 6 and the previous one.

  • Symfony 5.4 requires PHP 7.2.5 or higher
  • Symfony 6 Requires PHP 8.0.2 or higher

One thinking about a new API development project, one of the crucial ideas is to pick the right framework. Around numerous web technologies, PHP has improved rapidly, presently being used broadly for building straightforward as well as complex web applications.

Installation

The following commands configure an executable file. To make sure so that I can execute this file from anywhere on the machine, we follow the command’s advice and move the file somewhere else.

curl -sS https://get.symfony.com/cli/installer | bashmv /Users/mert/.symfony/bin/symfony /usr/local/bin/symfonysymfony new very_first_api
cd very_first_api && symfony server -d

The first thing first is creating a new application with the third command. Thus, it will be installing and configuring requirements for us. According to the documentation, this command isn’t doing anything special. It clones a Git repository called symfony/skeleton and then uses Composer to install that project's dependencies. With the last command, we run a web server (127.0.0.1:8000), and actually, it uses PHP built-in web server.

How do we use Symfony?

From now on, I’d like to cover how we used PHP 8 in Symfony 6. Let’s dive into the “RequestSubscriber” concept in Symfony. After that, I’ll cover how can we initialize services automatically by Factory Service. Lastly, we will see the messenger component in Symfony.

RequestSubscriber

Obviously, it works like middleware. Once our application gets a request from anywhere, this subscriber is working as a door. It handles all of them first. Thus we can define the object values that use everywhere in the entire app. I created a directory named “src/EventSubscriber” and a file named “RequestSubscriber”. Let’s log all requests into a file. We implement “ProviderControllerInterface” for some of the controllers and we wanted to log for only these controllers. Firstly, we correct the controllers that we want. After that, if there is no exception, it will log the requests for these controllers.

// src/EventSubscriber/RequestSubscriber.php
class RequestSubscriber implements EventSubscriberInterface
{
public function __construct(
private ControllerResolverInterface $controllerResolver
)
{
}
public static function getSubscribedEvents(): array
{
return [
RequestEvent::class => 'onKernelRequest'
];
}
public function onKernelRequest(RequestEvent $event)
{
try {
$controller = $this->controllerResolver >getController($event->getRequest());
if (is_array($controller)) {
$controller = $controller[0];
}
if ($controller instanceof ProviderControllerInterface) {
//it will be writing POST request body in project_dir/var/log/dev.log file
$this->logger->info(json_encode($event->getRequest()->toArray(), TRUE));
}
} catch (Routing\Exception\ResourceNotFoundException $exception) {
return;
} catch (\Exception $exception){
return;
}
}
}

You will the body logs in var/log/dev.log file what you send to your app. For instance:

[2022-01-17T09:42:29.526106+01:00] app.INFO: {"Login":"28542_mertsimsek","OperatorId":2192,"Session":""} [] []

Factory Service

Basically, we need to have one service for each game provider. Therefore we created them but we had a lot of services, we needed to initialize them automatically. So we created a file like the following in the services directory.

// src/Service/FactoryProviderService.php
class FactoryProviderService
{
public function __construct(private Container $container)
{}
}public function getService(ProviderEnum $provider): ProviderServiceInterface
{
return $this->container->get($provider->getServiceClass());
}
}

ProviderEnum file carries options of the providers. Let’s check out the following codes.

// src/Enum/ProviderEnum.php
enum ProviderEnum: int
{
case TestProvider = 111;
public function getServiceClass(): string
{
return match ($this) {
self::TestProvider => TestProvider::class,
};
}
public static function getRoutePrefixes(): array
{
return [
self::TestProvider->value => 'testprovider',
];
}
public static function fromRoutePrefix(string $routePrefix): self
{
return self::tryFrom(array_flip(self::getRoutePrefixes())[$routePrefix]);
}
}

Usage.

$providerService = $this->factoryProviderService->getService(ProviderEnum::fromRoutePrefix("testprovider"));

From now on, we will be able to have provider services by the provider names. When we pass to factory service a provider name, it will initialize its service file automatically.

Messenger

I can say that it is one of my favorite packages. Messenger provides a message bus with the ability to send messages and then handle them immediately in your application or send them through transports (e.g. queues) to be handled later. Install it with the following command.

composer require symfony/messenger

After having the package, we created a message to insert a row into the ElasticSearch.

// src/Message/ElasticMessage.php
class ElasticMessage
{
private $data;
public function __construct(array $data)
{
$this->data = $data;
}
public function getData(): array
{
return $this->data;
}
}

Who handles these messages? So, let’s create a handler for it. If there is a message in the bus/queue, this file will invoke it with a command. Like a sender and receiver, its concepts really useful and straightforward.

// src/MessageHandler/ElasticMessageHandler.php
class ElasticMessageHandler implements MessageHandlerInterface
{
public function __invoke(ElasticMessage $data, ElasticService $elasticService)
{
$elasticService->send($data);
}
}

As a bus/queue, we have chosen the “Redis” service to handle. So we updated the following default config file. Those parameters are coming from “.env.local” file.

// config/packages/messenger.yaml
framework:
messenger:
transports:
redis:
dsn: '%env(REDIS_TRANSPORT)%'
options:
consumer: "%env(MESSENGER_CONSUMER_NAME)%"
routing:
App\Message\ElasticMessage: redis

Sending “ElasticMessage” into the Redis will be like the following.

class DefaultController extends AbstractController
{
public function index(MessageBusInterface $bus)
{
$bus->dispatch(
(new ElasticMessage())
->setData($data)
);
}
}

The concept like we have a message and we send it from “DefaultController” by setting its data. After that, there will be a handler and it sends this message to the Redis. Last but not least is consuming this message by a command. We can do this with the messenger:consume command. It’s brilliant!

php bin/console messenger:consume async -vv

ResponseSubscriber

Let’s say we want to log something into 3rd party services such as Logstash, Graylog, Grafana, etc. We exactly do this in ResponseSubscriber. For example, let’s log all responses into a file for now. It will handle all responses and it will write them into the log file.

class ResponseSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return [
ResponseEvent::class => 'onKernelResponse',
];
}
/**
* @param ResponseEvent $event
* @return void
*/
public function onKernelResponse(ResponseEvent $event)
{
//it will be writing response body in project_dir/var/log/dev.log file
$this->logger->info(json_decode(json_encode($event->getResponse()->getContent(), TRUE)));
}
}
}

To Sum Up

I was very excited to use PHP 8. I’d like to see the interesting additions and new features. I hope this article will help you to move previous versions. In addition to that, for me, Symfony is more than a framework, it’s a philosophy. Thanks to it, we have got modern and complex structures and web services. It is the perfect technology. This is because Symfony is a flexible tool that allows you to easily modify or develop code that has already been created. In this post, I tried to cover how do we use some components and packages in Symfony to get an app in harmony. As much as we can, we follow up on the standards always.

Resources

--

--

Mert Simsek
Beyn Technology

I’m a software developer who wants to learn more. First of all, I’m interested in building, testing, and deploying automatically and autonomously.