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
- Web Development with Symfony: An application capable of dispatching push notifications to mobile devices via FCM [part 4]
- Web Development with Symfony: An application capable of dispatching push notifications to mobile devices via FCM [part 3]
- Web Development with Symfony: An application capable of dispatching push notifications to mobile devices via FCM [part 2]
- Web Development with Symfony: An application capable of dispatching push notifications to mobile devices via FCM [part 1]
Posted on Utopian.io - Rewarding Open Source Contributors
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
Community-Driven Witness!
I am the first and only Steem Community-Driven Witness. Participate on Discord. Lets GROW TOGETHER!
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