Commentaires
Conteneur de services: créer ses propres tags
Bonjour à tous, chers lecteurs. Aujourd'hui, je vous propose de poursuivre
nos tutoriels avec une présentation pratique du fonctionnement interne de la
compilation du conteneur de services et plus particulièrement des tags.
Court rappel sur les tags
L'objectif principal des tags est de fournir un moyen à votre bundle de
fournir des services ou des fonctionnalités qui peuvent être très facilement
étendus. En bref, un tag c'est un point d'extension !
Parmi les tags les plus connus vous avez sûrement déjà utilisé :
-
form.type
qui permet d'enregistrer un nouveau type de
formulaire,
-
doctrine.event_(listener|subscriber)
qui enregistre un
nouvel écouteur au cycle d'évènements de Doctrine,
-
kernel.event_(listener|subscriber)
pour intercepter les
évènements du noyau,
-
twig.extension
pour charger une nouvelle extension dans
Twig,
-
... la liste plus complète est ici
Je vous propose aujourd'hui de faire un service de recherche de points
d'intérêt extensible via les tags.
Un service de recherche d'endroit
Puisque le sujet ne porte pas sur la géolocalisation, la recherche, les
services web ou encore les licornes... je vous propose de définir une
interface de recherche simple.
namespace MSB\LocatorBundle\Places;
interface PlaceLocatorInterface
{
/**
* Searches places given a query.
*
* @param string $query
*
* @return array
*/
public function searchByKeyword($query);
}
C'est l'interface de recherche la plus simple du monde, celle que madame
Michu sait très bien utiliser, et qui a fait gagner beaucoup d'argent à
Google.
Un simple appel de la méthode searchByKeyword
avec une chaine
de caractères doit retourner un tableau de résultats.
Je vous propose deux implémentations de cette interface afin de rendre les
choses intéressantes.
Rechercher dans Google Place API
Cette première implémentation consomme
l'API Place
de Google. Comme vous pouvez le voir, la clef d'authentification est obtenue
par le constructeur qui la transmettra lors de la construction de
l'url.
namespace MSB\LocatorBundle\Places;
/**
* GooglePlaceLocator searches for places into the Google Place API
*/
class GooglePlaceLocator implements PlaceLocatorInterface
{
private $key;
/**
* @param string $key The google API key
*/
public function __construct($key)
{
$this->key = $key;
}
public function searchByKeyword($query)
{
// url encode query
$urlEncodedQuery = urlencode($query);
// build query url
$url = sprintf('https://maps.googleapis.com/maps/api/place/textsearch/json?sensor=true&key=%s&query=%s', $this->key, $urlEncodedQuery);
// fetch and decode url
$json = json_decode(file_get_contents($url), true);
// transform every results into [name, address, source]
return array_map(function($result) {
return [
'name' => $result['name'],
'address' => $result['formatted_address'],
'source' => 'Google',
];
}, $json['results']);
}
}
Notez que même si Google répond quantité d'informations, je me suis contenté
de récupérer le nom et l'adresse de l'endroit. J'ai également adjoint la
source du résultat, à savoir Google
. Avoir un format uniforme
sera utile quand on va y ajouter des résultats fournis par un autre
service.
L'accès à
l'API et à
la documentation de Google sont
ici.
Rechercher dans Here Place API
Ma seconde implémentation est très similaire à la première, mais pour le
service de localisation de Nokia Here.
namespace MSB\LocatorBundle\Places;
/**
* HerePlaceLocator searches for places into the Here Place API
*/
class HerePlaceLocator implements PlaceLocatorInterface
{
private $appId;
private $appCode;
/**
* @param string $appId The here app id
* @param string $appCode The here app code
*/
public function __construct($appId, $appCode)
{
$this->appId = $appId;
$this->appCode = $appCode;
}
public function searchByKeyword($query)
{
// url encode query
$urlEncodedQuery = urlencode($query);
// build query url
$url = sprintf('http://places.cit.api.here.com/places/v1/discover/search?at=48.85031735791848,2.3450558593746678&app_id=%s&app_code=%s&q=%s', $this->appId, $this->appCode, $urlEncodedQuery);
// fetch and decode url
$json = json_decode(file_get_contents($url), true);
// transform every results into [name, address, source]
return array_map(function($result) {
return [
'name' => $result['title'],
'address' => str_replace('<br/>', ', ', $result['vicinity']),
'source' => 'Here',
];
}, $json['results']['items']);
}
}
Diverses choses sont à noter :
-
L'API
de Here est beaucoup plus
REST
que celle de Google, mais vous verrez cela plus tard...
-
Sinon, elle a un défaut majeur : elle a besoin d'avoir des coordonnées
pour faire une recherche (paramètre
at
de l'url). Du coup,
j'ai mis les coordonnées de Paris, mais ça posera d'évidentes
limitations dans les résultats. Aussi, Here
retourne l'adresse au format
html avec des
<br/>
dedans que j'ai du ôter.
Vous pouvez obtenir un accès à
l'API de
HERE et à la documentation
ici.
L'implémentation du service extensible
Maintenant, je vous propose un troisième service, qui sera celui qui
permettra d'agréger les résultats provenant d'autres implémentations.
namespace MSB\LocatorBundle\Places;
/**
* ChainedPlaceLocator searches for places into registered
* implementations of PlaceLocatorInterface.
*/
class ChainedPlaceLocator implements PlaceLocatorInterface
{
private $locators = [];
/**
* Registers a new implementation of PlaceLocatorInterface
*
* @param PlaceLocatorInterface $locator
*/
public function addLocator(PlaceLocatorInterface $locator)
{
$this->locators[] = $locator;
}
public function searchByKeyword($query)
{
$results = [];
// for each implementations...
foreach($this->locators as $locator) {
// ...merge its results
$results = array_merge($results, $locator->searchByKeyword($query));
}
return $results;
}
}
Comme vous pouvez le voir, une méthode addLocator
permet
d'enregistrer une nouvelle implémentation de l'interface
PlaceLocatorInterface
. Ainsi, à l'appel de la méthode
searchByKeyword
, on obtiendra l'agrégation de tous les
résultats des implémentations.
On va donc créer un tag place_locator
qui permettra
d'identifier les services qui doivent être automatiquement enregistrés
dans la méthode ChainedPlaceLocator::addLocator
.
Déclaration des services et du tag
Pour commencer, voici la déclaration des précédents services, contenant
également l'utilisation du fameux tag :
<services>
<service id="msb.places.chained_locator" class="MSB\LocatorBundle\Places\ChainedPlaceLocator">
</service>
<service id="msb.places.google_locator" class="MSB\LocatorBundle\Places\GooglePlaceLocator">
<argument>YouNeedToGetAnAccountAnPutYourKeyHere</argument>
<tag name="place_locator" />
</service>
<service id="msb.places.here_locator" class="MSB\LocatorBundle\Places\HerePlaceLocator">
<argument>DemoAppId01082013GAL</argument>
<argument>AJKnXv84fjrb0KIHawS0Tg</argument>
<tag name="place_locator" />
</service>
</services>
On remarquera l'injection des identifications dans les constructeurs des
services de Google et de Here, ainsi que le placement du tag
<tag name="place_locator" />
.
Maintenant, nous allons ajouter une passe de compilation
aka CompilerPass.
Les passes de compilation sont des classes qui sont appelées au moment où le
conteneur de dépendance est construit. Durant cette construction, il y a
plusieurs étapes pendant lesquelles les fichiers de déclaration de service
sont lus, les paramètres et la configuration sont résolus, les alias sont
créés et les tags sont traités...
Cette construction aboutit à la création du conteneur compilé écrit dans un
fichier du cache (/app/**/app***ProjectContainer.php
). Pour des
raisons évidentes de performance, cette compilation n'a lieu qu'une seule
fois en environnement de production (prod
), mais elle a lieu à
chaque fois pour l'environnement de développement (dev
) pour
éviter d'enchaîner la suppression manuelle du cache lors du développement.
Notre passe de compilation aura pour rôle de récupérer la définition du
service msb.places.chained_locator
afin d'ajouter dynamiquement
un appel à méthode addLocator
autant de fois qu'il y a de
définitions de services ayant le tag place_locator
.
namespace MSB\LocatorBundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
class PlaceLocatorPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
// get the msb.places.chained_locator service definition
$definition = $container->findDefinition('msb.places.chained_locator');
// for every service tagged place_locator...
foreach ($container->findTaggedServiceIds('place_locator') as $id => $tags) {
// ... add it as a call to addLocator of the msb.places.chained_locator service definition
$definition->addMethodCall('addLocator', [new Reference($id)]);
}
}
}
Remarquez ici que nous ne manipulons par les services directement. C'est
très important, car durant la phase de compilation, ceux-ci ne sont pas
encore réellement créés. En fait nous manipulons la définition du service.
La définition contient la suite d'instructions à exécuter pour créer et
configurer le service proprement à l'exécution. Certaines d'entre elles
appellent la méthode addLocator
avec chacun des services
taggés.
Ainsi en manipulant la définition du service, la passe de compilation permet
de repousser à plus tard la création du service (et de ceux qui lui sont
associés). De plus, parce que le conteneur est en cache, le coût en
performance est très réduit, voire quasiment nul.
La dernière étape consiste à enregistrer la passe de compilation dans le
framework. Pour y parvenir, il s'agit simplement de surcharger la méthode
build
de la classe du bundle.
namespace MSB\LocatorBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use MSB\LocatorBundle\DependencyInjection\Compiler\PlaceLocatorPass;
class MSBLocatorBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
parent::build($container);
$container->addCompilerPass(new PlaceLocatorPass());
}
}
Bonus, trouver un bar pour rencontrer
l'AFSY
Parce qu'un service qu'on n'utilise pas, c'est triste, je vous propose une
rapide commande qui permet d'obtenir des résultats.
namespace MSB\LocatorBundle\Command;
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class LocatePlaceCommand extends ContainerAwareCommand
{
protected function configure()
{
$this
->setName('msb:locate-place')
->addArgument('query', InputArgument::REQUIRED, 'What to search');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$output->writeln(sprintf('Looking for <comment>%s</comment>', $input->getArgument('query')));
// get the place locator
$placeLocator = $this->getContainer()->get('msb.places.chained_locator');
// fetch the results
$results = $placeLocator->searchByKeyword($input->getArgument('query'));
// show the results
$output->writeln(sprintf('Found <info>%d</info> result(s)', count($results)));
foreach ($results as $result) {
$output->writeln(sprintf('<info>%s</info> by <comment>%s</comment>', $result['name'], $result['source']));
$output->writeln(sprintf(' %s', $result['address']));
}
return 0;
}
}
Et voici, une sélection de bars recommandés par Google et
Here où vous aurez des chances de pouvoir rencontrer
les gentils membres de l'AFSY qui se tiennent aux rendez-vous tous les
mois.
D:\Users\Michel\Documents\Projects\symfony-standard>php app/console msb:locate-place "Bar paris"
Looking for Bar paris
Found 40 result(s)
Downtown Cafe by Google
46 Rue Jean-Pierre Timbaud, Paris, France
Le Harry's New York Bar by Google
5 Rue Daunou, Paris, France
Buddha Bar by Google
8-12 Rue Boissy d'Anglas, Paris, France
La Palette by Google
43 Rue de Seine, Paris, France
[...]
L'Atelier de Joël Robuchon by Here
5 Montalembert Rue, 75007 Paris, France
Pont-Royal by Here
7 Montalembert Rue, 75007 Paris, France
Au Bon Coin by Here
50 Rue Lemercier, 75017 Paris, France
Quand même, sacré Joël, on voit qu'il garde la côte !
Wrap Up!
J'espère que cette découverte du conteneur de services et des tags vous a
plu et qu'elle vous aidera à faire des services plus facilement
extensibles.
Vous pouvez retrouver les sources du Bundle de cet article sur mon
github.