PHP     Symfony 2 et 3       AngularJS   Angular2       Cordova/Ionic 2
Daniel G.
Développeur d'applications Web et mobiles - Gironde

TUTORIEL

Comment intégrer une librairie PHP externe dans Symfony2 sans pour autant en faire un bundle. Par exemple, vous avez récupéré ou développé une librairie qui fait une tâche bien précise et qui demande peu de configuration, très simple d'utilisation ça ne vaut pas le coup d'en faire un bundle.
Et donc voici comment l'intégrer dans Symfony2 de la façon la plus simple à la bonne pratique.

Le Projet

Nous allons utiliser la librairie Geocoder comme exemple pour ce tutoriel. Cette librairie demande une adresse IP et il renvoit une adresse postale.
( Il existe un bundle sur geocoder mais pour notre exemple, nous allons bien sur l'ignorer )

INSTALLATION

Geocoder : Determiner la localisation à partir d'une adresse IP.
Le site officiel : http://geocoder-php.org/

Télecharger cette version : geocoder v2.8.1
https://github.com/geocoder-php/Geocoder/releases/tag/2.8.1
La création du projet Symfony2 :

            symfony new geo_project 2.8
        
copier le dossier Geocoder-2.8.1 dans le projet symfony à cette emplacement : .../src/lib
pour obtenir : geo_project/src/lib/Geocoder-2.8.1


Rendre disponible la librairie geocoder dans le projet Symfony :
composer.json

            "autoload": {
                ...
                ,
                "psr-0": {
                    "": "src/lib/Geocoder-2.8.1/src/"
                }
                ,
                ...
        

recalculer l'autoload : (la nouvelle librairie sera inclus dans le loader. Nous n'aurons pas besoin donc de faire d'import...)

            composer dump-autoload
        

UTILISATION

Voyons 3 manières de faire :
1. Limité : Utilisation de la librairie dans une action d'un controller (sans service...)
2. Mieux : Rendre les classes de la librairie en service et les appeller dans une action d'un controller
3. Bonne pratique : Utiliser un service dédié à la gestion de la librairie et appel de celui ci dans une action d'un controller


1. Limité : Utilisation de la librairie dans une action d'un controller (sans service...) :

DefaultController.php

            /**
             * @Route("/geocoder", name="geocoder")
             */
            public function geoCoderAction(Request $request) {
                $adapter  = new \Geocoder\HttpAdapter\CurlHttpAdapter();
                $provider = new \Geocoder\Provider\FreeGeoIpProvider($adapter);
                $geocoder = new \Geocoder\Geocoder($provider);

                $result   = $geocoder->geocode('64.15.116.91');     // IP de google.fr

                dump($result); exit;
            }
        

            http://geo-project.dev/app_dev.php/geocoder
        
Résultat :

                DefaultController.php on line 23:
                    Geocoded {
                  #latitude: 37.4192
                  #longitude: -122.0574
                    ...
                  #city: "Mountain View"
                  #zipcode: "94043"
                    ...
            
2. Mieux : Rendre les classes de la librairie en service et les appeller dans une action d'un controller :

        $adapter  = new \Geocoder\HttpAdapter\CurlHttpAdapter();
        $provider = new \Geocoder\Provider\FreeGeoIpProvider($adapter);
        $geocoder = new \Geocoder\Geocoder($provider);
        
Vu dans le 1., Nous allons simplement remplacer le code ci dessus en service :

config.yml

            services:
                geocoder_adapter:
                    class: Geocoder\HttpAdapter\CurlHttpAdapter
                    public: false
                geocoder_provider:
                    class: Geocoder\Provider\FreeGeoIpProvider
                    public: false
                    arguments: [@geocoder_adapter]
                geocoder:
                    class: Geocoder\Geocoder
                    calls:
                        - [registerProviders, [[@geocoder_provider]]]
        

Dans une action d'un controller, un simple appel au service geocoder (qui lui même fait appel geocoder_provider et ce dernier à geocoder_adapter) :
DefaultController.php

            ...
            public function geoCoderAction(Request $request) {
                $result = $this->get('geocoder')->geocode('64.15.116.91');

                dump($result); exit;
            }
        
Résultat :

                DefaultController.php on line 23:
                    Geocoded {
                  #latitude: 37.4192
                  #longitude: -122.0574
                    ...
                  #city: "Mountain View"
                  #zipcode: "94043"
                    ...
            
3. Bonne pratique : Utiliser un service dédié à la gestion de la librairie et appel de celui ci dans une action d'un controller :

Remarque :
Nous avons vu que pour géocaliser une IP nous devons choisir un adapter et un provider.
Liste des adapter : BuzzHttpAdapter CurlHttpAdapter GeoIP2Adapter GuzzleHttpAdapter ...
Liste des provider : ArcGISOnlineProvider BaiduProvider BingMapsProvider ...

Notre projet va utiliser curl comme moyen technique et nous allons interroger plusieurs providers afin de s'assurer d'avoir au moins une réponse parmi les providers.

Et donc le but est de créer une classe qui va geocaliser l'ip en parcourant 1 ou plusieurs provider(s) via un adapter(contrairement au point 1. et 2. ou les 2 élements sont figés).
On injectera dans cette classe un adapter et plusieurs providers via les services Symfony.

services.yml

    services:
        geocoder:
            class: Geocoder\Geocoder
            public: false

        curl_http_adapter:
            class: Geocoder\HttpAdapter\CurlHttpAdapter
            public: false

        free_geo_ip_provider:
            class: Geocoder\Provider\FreeGeoIpProvider
            public: false
            arguments: [@curl_http_adapter]
            tags:
                - { name: app.list.provider }

        geo_plugin_provider:
            class: Geocoder\Provider\GeoPluginProvider
            public: false
            arguments: [@curl_http_adapter]
            tags:
                - { name: app.list.provider }

        user_geocoder:
            class: AppBundle\services\Geocoder\UserGeocoder
            scope: request
            arguments: [@request, @geocoder]
            calls:
                - [addGeocoder, [@free_geo_ip_provider]]
                - [addGeocoder, [@geo_plugin_provider]]
        
remarque : Vous ajoutez ou supprimez ci dessus vos providers (sans modifier de classe...) :

            xxxxxx_provider:
                ...

            user_geocoder:
                ...
                calls:
                    ...
                    - [addGeocoder, [@xxxxx_provider]]
          

remarque :
tags:
    - { name: app.list.provider }

Pour l'instant les tags des services "provider" ne sont pas utilisés, ils serviront plus loin pour le compiler Passes.


...\AppBundle\services\UserGeocoder.php

            ...
            namespace AppBundle\services\Geocoder;

            use Geocoder\Geocoder;
            use Geocoder\GeocoderInterface;
            use Geocoder\Provider\ProviderInterface;
            use Symfony\Component\HttpFoundation\Request;


            class UserGeocoder {
                protected $geocoder;
                protected $providers = [];
                protected $user_ip;

                public function __construct(Request $request, GeocoderInterface $geocoder)   {
                    $this->geocoder = $geocoder;

                    $this->user_ip = $request->getClientIp();
                    if ($this->user_ip == '127.0.0.1') {
                        $this->user_ip = '64.15.116.91';    // google.fr
                    }
                }

                public function addGeocoder(ProviderInterface $provider) {
                    $this->providers[] = $provider;
                }

                public function getGeocoder() {
                    $res = null;
                    // parcourir les providers jusqu'à obtenir une réponse positive.
                    foreach ($this->providers as $provider) {
                        $this->geocoder->registerProvider($provider);
                        $res = $this->geocoder->geocode($this->user_ip);
                        if ($res->getCounty()!=null)
                            break;
                    }

                    return $res;
                }
            }
        
DefaultController.php

            ...
            public function geoCoderAction(Request $request) {
                dump( $this->get('user_geocoder')->getGeocoder() );
                exit;
            }
        
Résultat :

                DefaultController.php on line 23:
                    Geocoded {
                  #latitude: 37.4192
                  #longitude: -122.0574
                    ...
                  #city: "Mountain View"
                  #zipcode: "94043"
                    ...
            

Bonus

: Compiler Passes :

Les passes de compilation vous donnent l’opportunité de manipuler d’autres définitions de service qui ont été définies via le conteneur de service.

services.yml

            ...
        user_geocoder:
            class: AppBundle\services\Geocoder\UserGeocoder
            scope: request
            arguments: [@request, @geocoder]
            calls:
                - [addGeocoder, [@free_geo_ip_provider]]
                - [addGeocoder, [@geo_plugin_provider]]
    
Le service user_geocoder fait 2 fois appel à addGeocoder. Pour un traitement plus complexe, il est possible d'utiliser un Compiler Passes. Ce que nous allons faire.

D'abord, nous enlevons ces 2 tâches dans le service :
services.yml

        ...
        user_geocoder:
            class: AppBundle\services\Geocoder\UserGeocoder
            scope: request
            arguments: [@request, @geocoder]
    


Nous allons créer le compiler passes pour effectuer ces 2 tâches :
AppBundle\DependencyInjection\Compiler\UserGeocoderPass.php

            namespace AppBundle\DependencyInjection\Compiler;

            use Symfony\Component\DependencyInjection\ContainerBuilder;
            use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
            use Symfony\Component\DependencyInjection\Reference;

            class UserGeocoderPass implements CompilerPassInterface {
                public function process(ContainerBuilder $container)     {
                    if (!$container->hasDefinition('user_geocoder'))         {
                        return;
                    }
                    $service_definition = $container->getDefinition('user_geocoder');
                    $tagged = $container->findTaggedServiceIds('app.list.provider');    //  tags -> 'app.list.provider' -> services.yml

                    foreach ($tagged as $id => $attrs) {
                        $service_definition->addMethodCall(
                            'addGeocoder',
                            [new Reference($id)]
                        );
                    }
                }
            }
    


Indiquer au bundle AppBundle d'executer le Compiler Passes :
\AppBundle\AppBundle.php

        namespace AppBundle;

        use Symfony\Component\HttpKernel\Bundle\Bundle;

        use Symfony\Component\DependencyInjection\ContainerBuilder;
        use AppBundle\DependencyInjection\Compiler\UserGeocoderPass;

        class AppBundle extends Bundle
        {
            public function build(ContainerBuilder $container)    {
                parent::build( new ContainerBuilder());
                $container->addCompilerPass(new UserGeocoderPass());
            }
        }
    


Remarque :
Ce que nous faisons avec le compiler passes c'est la definition du code à éxecuter. En effet, c'est au moment ou nous sollicitons les services que la definition du compiler passes est executé.