Web Development with Symfony: An application capable of dispatching push notifications to mobile devices via FCM [part 5]

in #utopian-io7 years ago (edited)

email-marketing-2362038_640.png

Source: pixabay.com. Licensed under CC0 Creative Commons

What will I learn?

  • How to create a friendly bundle configuration
  • How to define an ORM relationship with an interface

Requirements

  • UNIX/Linux based OS
  • Apache2 server with PHP7 installed
  • MySQL database
  • Text editor of choice
  • Existing Firebase project
  • Composer
  • Base project found here

Difficulty

  • Intermediate

Tutorial contents

This tutorial is a fifth one in the Web Development series where we jump into details on how to develop applications with Symfony using a sample project capable of sending push notifications to mobile devices. In the previous article the process of creating an authentication system for a RESTful API, which allowed to secure resources with an api key, was described.

A sample project used with this tutorial can be found here. It is basically the code you will end up with after completing the previous tutorial. It is recommended to clone the aforementioned project from the given github repository.

What aspects of Symfony web development will be covered in this tutorial?

  • The process of creating a friendly configuration for a bundle with the use of the Security Component, which allows to define high level parameters to be used by a bundle to make complex changes based on the provided configuration.
  • The process of defining an ORM association mapping that references an interface instead of an entity concrete class to avoid hard coupling between different classes.

How to create a friendly bundle configuration?

The process of defining base values for a bundle can be centralised thanks to the Config Component which comes pre-packaged with Symfony framework. These values can be used e.g. as container parameters to ease the service configuration process.

Note: The Config Component related files are stored within a DependencyInjection directory.

A file called src/Ptrio/MessageBundle/DependencyInjection/Configuration.php allows for setting an array of arrays which elements are used for a bundle configuration. A TreeBuilder type object can be mainly helpful for setting defaults for a particular bundle and adding validation to configuration options, thence preventing from using incorrect values.

A configuration tree is created within a Configuration::getConfigTreeBuilder() method body.

<?php
// src/Ptrio/MessageBundle/DependencyInjection/Configuration.php
namespace App\Ptrio\MessageBundle\DependencyInjection;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
class Configuration implements ConfigurationInterface
{
    public function getConfigTreeBuilder()
    {
        $treeBuilder = new TreeBuilder();
        $rootNode = $treeBuilder->root('ptrio_message');
        // Some logic responsible for building a configuration tree...
        return $treeBuilder;
    }
}

As shown in the example above, a root configuration node should be set first. A ptrio_message value is a bundle dependency injection alias, which consists of a lowercase vendor name and a bundle name separated by a underscore character.

Defining a configuration tree

As of now, a PtrioMessageBundle bundle contains definitions capable of creating a configuration for a Firebase client.

        // src/Ptrio/MessageBundle/DependencyInjection/Configuration.php
        $rootNode
            ->children()
                ->arrayNode('firebase')
                    ->children()
                        ->scalarNode('api_url')->isRequired()->cannotBeEmpty()->end()
                        ->scalarNode('server_key')->isRequired()->cannotBeEmpty()->end()
                    ->end()
                ->end()
            ->end()
        ;

The api_url and server_key options are both required (have to be set) and cannot contain empty values. An array produced with the above configuration tree is presented below.

[
    [
        'firebase' => [
            'api_url' => 'firebase_api_base_url',
            'server_key' => 'firebase_project_server_key',
        ],
    ],
]

A PtrioMessageBundle bundle configuration tree must be updated in a way that will allow for defining a ptrio_message.model.device.class container parameter value (which contains a fully-qualified device entity class name) inside a config/packages/ptrio_message.yaml file.

Previously device entity class name was separated out to a ptrio_message.model.device.class parameter directly in a services.yaml service configuration file. That approach however has some downsides - it does not allow for using a value stored in the aforementioned parameter to provide more complex configuration logic and it is also impossible to use validation with it.

Start by adding the following definitions to a TreeBuilder object’s root node.

                ->arrayNode('device')
                    ->children()
                        ->arrayNode('classes')
                            ->children()
                                ->scalarNode('model')->isRequired()->cannotBeEmpty()->end()
                            ->end()
                        ->end()
                    ->end()
                ->end()

As shown in the example, a model option has to be set and also it cannot contain an empty value.

An array presented below will be created from the above configuration tree.

[
    [
        // other array elements
        'device' => [
            'classes' => [
                'model' => 'App\Ptrio\MessageBundle\Entity\Device',
            ]
        ]
    ]
]
Loading a bundle configuration

An array created with the help of a TreeBuilder object will be passed as the first argument of a Extension::load(array $configs, ContainerBuilder $container) method, which then will be used during a bundle configuration process.

To preserve order in a PtrioMessageExtension class, which takes care of loading a bundle configuration, it is recommended to separate out logic responsible for a device related configuration to a class-scope method.

    /**
     * @param array $config
     * @param ContainerBuilder $container
     */
    private function loadDevice(array $config, ContainerBuilder $container)
    {
        $container->setParameter('ptrio_message.model.device.class', $config['classes']['model']);
    }

For now there is not much going on inside a PtrioMessageExtension::loadDevice(array $config, ContainerBuilder $container) method - a value contained in a $config['classes']['model’] array element is assigned to a ptrio_message.model.device.class container parameter. In the future, the aforementioned method might handle more complex operations.

Next, call a PtrioMessageExtension::loadDevice(array $config, ContainerBuilder $container) method inside a PtrioMessageExtension::load(array $configs, ContainerBuilder $container) method, which is responsible for loading a specific configuration.

    public function load(array $configs, ContainerBuilder $container)
    {
        $loader = new YamlFileLoader($container, new FileLocator(dirname(__DIR__).'/Resources/config'));
        $loader->load('services.yaml');
        $configuration = $this->getConfiguration($configs, $container);
        $config = $this->processConfiguration($configuration, $configs);
        $container->setParameter('ptrio_message.firebase.api_url', $config['firebase']['api_url']);
        $container->setParameter('ptrio_message.firebase.server_key', $config['firebase']['server_key']);
        $this->loadDevice($config['device'], $container); // add this line
    }

Now, a value for aptrio_message.model.device.class container parameter can be set directly in a config/packages/ptrio_message.yaml file.

    device:
        classes:
            model: 'App\Ptrio\MessageBundle\Entity\Device'

Finally, a ptrio_message.model.device.class parameter definition can be removed from a src/Ptrio/MessageBundle/Resources/config/services.yaml file, since it will no longer be necessary.

Note: For consistency, it is recommended to apply the same activities for a configuration related to message and user objects.

How to define an ORM relationship with an interface?

Until now all the steps to avoid hard-coupling between classes were taken except for one. There is still a reference to a concrete device entity class within a Message entity class mapping.

The Doctrine library, since the 2.2 version, allows for defining loose dependencies with the use of a ResolveTargetEntityListener type object, which is responsible for replacing a targetEntity argument value during runtime. It means that an interface or an abstract class can be used for an association mapping, which is then rewritten with a concrete entity class name.

Updating a configuration tree

Begin by updating a configuration tree for a PtrioMessageBundle, so a device interface name can be defined.

Add an interface node to a classes node inside a src/Ptrio/MessageBundle/DependencyInjection/Configuration.php file.

                        ->arrayNode('classes')
                            ->children()
                                ->scalarNode('model')->isRequired()->cannotBeEmpty()->end()
                                ->scalarNode('interface')->cannotBeEmpty()->defaultValue('App\Ptrio\MessageBundle\Model\DeviceInterface')->end()
                            ->end() // add this line

As shown in the example above, the default value for an interface is App\Ptrio\MessageBundle\Model\DeviceInterface.

Updating an extension class

A PtrioMessageExtension extension class should be amended, so a ResolveTargetEntityListener listener object will know which interface should be replaced with a given concrete entity class. To achieve that a PtrioMessageExtension class must implement a Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface interface.

class PtrioMessageExtension extends Extension implements PrependExtensionInterface

Then, it is necessary to add a PrependExtensionInterface::prepend(ContainerBuilder $container) method definition, which is capable of extending any given bundle configuration. In this particular case, a DoctrineBundle bundle configuration will be extended.

    public function prepend(ContainerBuilder $container)
    {
        $config = $this->processConfiguration(new Configuration(), $container->getExtensionConfig($this->getAlias()));
        $bundles = $container->getParameter('kernel.bundles');
        if (isset($bundles['DoctrineBundle'])) {
            $container
                ->loadFromExtension('doctrine', [
                    'orm' => [
                        'resolve_target_entities' => [
                            $config['device']['classes']['interface'] => $config['device']['classes']['model'],
                        ],
                    ],
                ])
            ;
        }
    }

Let’s go over some of the most important aspects found within a PtrioMessageExtension::prepend(ContainerBuilder $container) method body.

First, a configuration for a PtrioMessageBundle bundle is loaded with a ContainerBuilder::getExtensionConfig($name). A $name argument is a bundle DI alias. Then this configuration is processed with a Extension::processConfiguration(ConfigurationInterface $configuration, array $configs) method, so the default configuration values can be merged with a configuration array produced with a TreeBuilder object.

In the next step, a kernel.bundles parameter value is looked up in a ContainerBuilder object, which contains a list of all bundles used with an application.

Finally, an algorithm checks whether a DoctrineBundle bundle is contained within the aforementioned list. If that is the case, a DoctrineBundle bundle configuration is extended with an interface definition that is supposed to be replaced with an entity class.

Updating an ORM association mapping

In the last step, replacing a targetEntity argument, found in a Message::$device association mapping, with an interface name is required.

    // src/Ptrio/MessageBundle/Entity/Message.php
    /**
     * @ORM\ManyToOne(targetEntity="App\Ptrio\MessageBundle\Model\DeviceInterface")
     * @ORM\JoinColumn(name="device_id", referencedColumnName="id")
     */
    protected $device;

Now, in this particular case, a App\Ptrio\MessageBundle\Model\DeviceInterface value will be replaced with App\Ptrio\MessageBundle\Entity\Device.

Curriculum



Posted on Utopian.io - Rewarding Open Source Contributors

Sort:  

Thank you for the contribution. It has been approved.

You can contact us on Discord.
[utopian-moderator]

Hey @piotr42 I am @utopian-io. I have just upvoted you!

Achievements

  • You have less than 500 followers. Just gave you a gift to help you succeed!
  • Seems like you contribute quite often. AMAZING!

Community-Driven Witness!

I am the first and only Steem Community-Driven Witness. Participate on Discord. Lets GROW TOGETHER!

mooncryption-utopian-witness-gif

Up-vote this comment to grow my power and help Open Source contributions like this one. Want to chat? Join me on Discord https://discord.gg/Pc8HG9x