Installation et mise en place du bundle FOSElastica pour Symfony 2.3 et Elasticsearch 1.3

Installation et mise en place du bundle FOSElastica pour Symfony 2.3 et Elasticsearch 1.3

Publié le 06/10/2014

1 - Démarrage d'Elasticsearch

Sur la VM Elasticsearch, on démarre ce dernier et on récupère l'ip de la VM pour l'utiliser plus tard dans la configuration de l'application(Tutorial concernant l'installation d'Elasticsearch).

Démarrage :

cd /home/elasticsearch-1.3.0/ bin/elasticsearch

Récupération de l'ip de la VM :

ifconfig

2 - Installation de Symfony 2.3

Sur notre serveur web (ici MAMP) on va créer un hôte et un socle SF 2.3 pour notre application address book :

sudo php composer.phar create-project symfony/framework-standard-edition /Applications/MAMP/htdocs/sf_address_book/ 2.3.0

3 - Installation du bundle FOSElastica, GedmoDoctrineExtensions et JmsSerializerBundle

Ajout du bundle dans le composer.json :

nano composer.json


//...
	"friendsofsymfony/elastica-bundle": "3.0.*@dev",
	"jms/serializer-bundle": "0.13.*@dev",
	"gedmo/doctrine-extensions": "v2.3.9"
//...

Mise à jour des vendors :

sudo php composer.phar update

Enregistrement du bundle dans le kernel :

nano app/config/AppKernel.php


// ...
// elastica
	new FOS\ElasticaBundle\FOSElasticaBundle(),
	new JMS\SerializerBundle\JMSSerializerBundle(),
// ...

4 - Creation du MainBundle

On va créer un MainBundle pour y stocker l'entité que l'on souhaite indexer. Dans le cas présent on va créer une entité contact avec laquelle on va pouvoir faire un carnet d'adresse avec recherche.

On va pouvoir créer, modifier, lire et supprimer des contacts. L'index d'Elasticsearch sera automatiquement mis à jour lors d'évènements Doctrine lors de la création, modification ou suppression de contacts.

Création du bundle :

php app/console generate:bundle --namespace=AddressBook/MainBundle Choisir le format du fichier de config en yml et laisser les valeurs par défaut pour les autres choix :

Configuration format (yml, xml, php, or annotation): yml

Création de l'entité contact :

php app/console doctrine:generate:entity

Création des propriétés de l'entité :

The Entity shortcut name: AddressBookMainBundle:Contact Configuration format (yml, xml, php, or annotation) [annotation]: Instead of starting with a blank entity, you can add some fields now. Note that the primary key will be added automatically (named id). Available types: array, simple_array, json_array, object, boolean, integer, smallint, bigint, string, text, datetime, datetimetz, date, time, decimal, float, blob, guid. New field name (press to stop adding fields): firstname Field type [string]: Field length [255]: New field name (press to stop adding fields): lastname Field type [string]: Field length [255]: New field name (press to stop adding fields): gender Field type [string]: boolean New field name (press to stop adding fields): address Field type [string]: text New field name (press to stop adding fields): zipCode Field type [string]: Field length [255]: New field name (press to stop adding fields): city Field type [string]: Field length [255]: New field name (press to stop adding fields): phone Field type [string]: Field length [255]: New field name (press to stop adding fields): email Field type [string]: Field length [255]: New field name (press to stop adding fields): Do you want to generate an empty repository class [no]? yes

Création de la base de données et mise à jour du schéma :

php app/console doctrine:database:create php app/console doctrine:schema:update --force Génération des routes, controller et vues pour le CRUD de l'entité Contact : php app/console doctrine:generate:crud --entity=AddressBookMainBundle:Contact --route-prefix=contact --with-write --format=yml

5 - Implémentation de FOSElastica et de GedmoDoctrineExtensions sur l'entité Contact de notre MainBundle

On modifie notre entité Contact comme suit :


<?php

namespace AddressBook\MainBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
use FOS\ElasticaBundle\Configuration\Search;

/**
 * Contact
 * @ORM\Table(name="contact")
 * @ORM\Entity(repositoryClass="AddressBook\MainBundle\Entity\ContactRepository")
 * @Search(repositoryClass="AddressBook\MainBundle\Entity\SearchRepository\ContactRepository")
 * @ORM\HasLifecycleCallbacks
 */
class Contact
{
    /**
     * @var integer
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @var \DateTime $created
     *
     * @Gedmo\Timestampable(on="create")
     * @ORM\Column(name="created_at", type="datetime")
     */
    private $createdAt;

    /**
     * @var \DateTime $updated
     *
     * @Gedmo\Timestampable(on="update")
     * @ORM\Column(name="updated_at", type="datetime")
     */
    private $updatedAt;

    /**
     * @var string
     *
     * @ORM\Column(name="firstname", type="string", length=255)
     */
    private $firstname;

    /**
     * @var string
     *
     * @ORM\Column(name="lastname", type="string", length=255)
     */
    private $lastname;

    /**
     * @var boolean
     *
     * @ORM\Column(name="gender", type="boolean")
     */
    private $gender;

    /**
     * @var string
     *
     * @ORM\Column(name="address", type="text")
     */
    private $address;

    /**
     * @var string
     *
     * @ORM\Column(name="zipCode", type="string", length=255)
     */
    private $zipCode;

    /**
     * @var string
     *
     * @ORM\Column(name="city", type="string", length=255)
     */
    private $city;

    /**
     * @var string
     *
     * @ORM\Column(name="phone", type="string", length=255)
     */
    private $phone;

    /**
     * @var string
     *
     * @ORM\Column(name="email", type="string", length=255)
     */
    private $email;


    /**
     * Get id
     *
     * @return integer 
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set createdAt
     *
     * @param \DateTime $createdAt
     * @return Contact
     */
    public function setCreatedAt($createdAt)
    {
        $this->createdAt = $createdAt;

        return $this;
    }

    /**
     * Get createdAt
     *
     * @return \DateTime
     */
    public function getCreatedAt()
    {
        return $this->createdAt;
    }

    /**
     * Set updatedAt
     *
     * @param \DateTime $updatedAt
     * @return Contact
     */
    public function setUpdatedAt($updatedAt)
    {
        $this->updatedAt = $updatedAt;

        return $this;
    }

    /**
     * Get updatedAt
     *
     * @return \DateTime
     */
    public function getUpdatedAt()
    {
        return $this->updatedAt;
    }


    /**
     * Set firstname
     *
     * @param string $firstname
     * @return Contact
     */
    public function setFirstname($firstname)
    {
        $this->firstname = $firstname;
    
        return $this;
    }

    /**
     * Get firstname
     *
     * @return string 
     */
    public function getFirstname()
    {
        return $this->firstname;
    }

    /**
     * Set lastname
     *
     * @param string $lastname
     * @return Contact
     */
    public function setLastname($lastname)
    {
        $this->lastname = $lastname;
    
        return $this;
    }

    /**
     * Get lastname
     *
     * @return string 
     */
    public function getLastname()
    {
        return $this->lastname;
    }

    /**
     * Set gender
     *
     * @param boolean $gender
     * @return Contact
     */
    public function setGender($gender)
    {
        $this->gender = $gender;
    
        return $this;
    }

    /**
     * Get gender
     *
     * @return boolean 
     */
    public function getGender()
    {
        return $this->gender;
    }

    /**
     * Set address
     *
     * @param string $address
     * @return Contact
     */
    public function setAddress($address)
    {
        $this->address = $address;
    
        return $this;
    }

    /**
     * Get address
     *
     * @return string 
     */
    public function getAddress()
    {
        return $this->address;
    }

    /**
     * Set zipCode
     *
     * @param string $zipCode
     * @return Contact
     */
    public function setZipCode($zipCode)
    {
        $this->zipCode = $zipCode;
    
        return $this;
    }

    /**
     * Get zipCode
     *
     * @return string 
     */
    public function getZipCode()
    {
        return $this->zipCode;
    }

    /**
     * Set city
     *
     * @param string $city
     * @return Contact
     */
    public function setCity($city)
    {
        $this->city = $city;
    
        return $this;
    }

    /**
     * Get city
     *
     * @return string 
     */
    public function getCity()
    {
        return $this->city;
    }

    /**
     * Set phone
     *
     * @param string $phone
     * @return Contact
     */
    public function setPhone($phone)
    {
        $this->phone = $phone;
    
        return $this;
    }

    /**
     * Get phone
     *
     * @return string 
     */
    public function getPhone()
    {
        return $this->phone;
    }

    /**
     * Set email
     *
     * @param string $email
     * @return Contact
     */
    public function setEmail($email)
    {
        $this->email = $email;
    
        return $this;
    }

    /**
     * Get email
     *
     * @return string 
     */
    public function getEmail()
    {
        return $this->email;
    }
}

Création du search repository :

nano src/AddressBook/MainBundle/Entity/SearchRepository/ContactRepository.php


<php
/**
 * Created by PhpStorm.
 * User: gerard
 * Date: 05/10/2014
 * Time: 20:06
 */

namespace AddressBook\MainBundle\Entity\SearchRepository;

use FOS\ElasticaBundle\Repository;

class ContactRepository extends Repository
{

} 

6 - Création du fichier de mapping

Le fichier de mapping va définir quels champs de notre model seront indexés.

Ajout des paramètres de connexion pour ElasticSearch dans le fichier parameters.yml.dist et parameter.yml :

parameters: elastic_host : localhost elastic_port : 9200

Création d'un fichier de config pour FOSElastica :

nano app/config/fos_elastica.yml


fos_elastica:
    clients:
        default: { host: %elastic_host%, port: %elastic_port% }
    indexes:
        addressbook:
            client: default
            types:
                contact:
                    mappings:
                        id:
                            type: integer
                        createdAt :
                            type : date
                        updatedAt :
                            type : date
                        gender:
                            type: boolean
                        firstname : ~
                        lastname : ~
                        address : ~
                        zipCode : ~
                        city:  ~
                        phone: ~
                        email: ~
                    persistence:
                        driver: orm
                        model: AddressBook\MainBundle\Entity\Contact
                        finder: ~
                        provider: ~
                        listener: ~

Ajout du fichier de conf. à config.yml :

nano app/config/config.yml


// ...
imports:
    - { resource: fos_elastica.yml }
// ...

7 - Création d'un objet de recherche pour l'entité Contact

On va créer un Model pour la recherche sur notre entité Contact :

nano src/AddressBook/MainBundle/Model/ContactSearch.php


<?php
/**
 * Created by PhpStorm.
 * User: gerard
 * Date: 05/10/2014
 * Time: 20:18
 */

namespace AddressBook\MainBundle\Model;

use Symfony\Component\HttpFoundation\Request;

class ContactSearch
{
    protected $dateFrom;

    protected $dateTo;

    protected $firstname;

    protected $lastname;

    protected $gender;

    protected $address;

    protected $zipCode;

    protected $city;

    protected $phone;

    protected $email;

    public function __construct()
    {
        // init dateFrom
        $date = new \DateTime();
        $month = new \DateInterval('P1Y');
        $date->sub($month);
        $date->setTime('00', '00', '00');

        $this->dateFrom = $date;
        $this->dateTo = new \DateTime();
        $this->dateTo->setTime('23', '59', '59');
    }

    /**
     * @param \DateTime $dateFrom
     * @return $this
     */
    public function setDateFrom(\DateTime $dateFrom)
    {
        if ($dateFrom != '')
        {
            $dateFrom->setTime('00', '00', '00');
            $this->dateFrom = $dateFrom;
        }

        return $this;
    }

    /**
     * @return \DateTime
     */
    public function getDateFrom()
    {
        return $this->dateFrom;
    }

    /**
     * @param \DateTime $dateTo
     * @return $this
     */
    public function setDateTo(\DateTime $dateTo)
    {
        if ($dateTo != '')
        {
            $dateTo->setTime('23', '59', '59');
            $this->dateTo = $dateTo;
        }

        return $this;
    }

    /**
     * @return \DateTime
     */
    public function getDateTo()
    {
        return $this->dateTo;
    }

    /**
     * clear dates
     */
    public function clearDates()
    {
        $this->dateFrom = null;
        $this->dateTo = null;
    }

    /**
     * @return mixed
     */
    public function getAddress()
    {
        return $this->address;
    }

    /**
     * @param mixed $address
     */
    public function setAddress($address)
    {
        $this->address = $address;
    }

    /**
     * @return mixed
     */
    public function getCity()
    {
        return $this->city;
    }

    /**
     * @param mixed $city
     */
    public function setCity($city)
    {
        $this->city = $city;
    }

    /**
     * @return mixed
     */
    public function getEmail()
    {
        return $this->email;
    }

    /**
     * @param mixed $email
     */
    public function setEmail($email)
    {
        $this->email = $email;
    }

    /**
     * @return mixed
     */
    public function getFirstname()
    {
        return $this->firstname;
    }

    /**
     * @param mixed $firstname
     */
    public function setFirstname($firstname)
    {
        $this->firstname = $firstname;
    }

    /**
     * @return mixed
     */
    public function getGender()
    {
        return $this->gender;
    }

    /**
     * @param mixed $gender
     */
    public function setGender($gender)
    {
        $this->gender = $gender;
    }

    /**
     * @return mixed
     */
    public function getLastname()
    {
        return $this->lastname;
    }

    /**
     * @param mixed $lastname
     */
    public function setLastname($lastname)
    {
        $this->lastname = $lastname;
    }

    /**
     * @return mixed
     */
    public function getPhone()
    {
        return $this->phone;
    }

    /**
     * @param mixed $phone
     */
    public function setPhone($phone)
    {
        $this->phone = $phone;
    }

    /**
     * @return mixed
     */
    public function getZipCode()
    {
        return $this->zipCode;
    }

    /**
     * @param mixed $zipCode
     */
    public function setZipCode($zipCode)
    {
        $this->zipCode = $zipCode;
    }


} 

8 - Création du formulaire de recherche

Création du FormType :

nano src/AddressBook/MainBundle/Form/Type/ContactSearchType.php


<?php
/**
 * Created by PhpStorm.
 * User: gerard
 * Date: 05/10/2014
 * Time: 20:24
 */

namespace AddressBook\MainBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use AddressBook\MainBundle\Model\ContactSearch;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class ContactSearchType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('gender', 'text' , array(
                'required' => false
            ))
            ->add('firstname', 'text', array(
                'required' => false
            ))
            ->add('lastname', 'text', array(
                'required' => false
            ))
            ->add('address', 'textarea', array(
                'required' => false
            ))
            ->add('zipCode', 'text', array(
                'required' => false
            ))
            ->add('city', 'text', array(
                'required' => false
            ))
            ->add('phone', 'text', array(
                'required' => false
            ))
            ->add('email', 'text', array(
                'required' => false
            ))
            ->add('search','submit')
        ;
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        parent::setDefaultOptions($resolver);
        $resolver->setDefaults(array(
            // avoid to pass the csrf token in the url (but it's not protected anymore)
            'csrf_protection' => false,
            'data_class' => 'AddressBook\MainBundle\Model\ContactSearch'
        ));
    }

    public function getName()
    {
        return 'Contact_search_type';
    }
}

Création du service pour le formulaire :

nano src/AddressBook/MainBundle/Resources/config/services.yml


Contact.form.search.type:
        class: AddressBook\MainBundle\Form\Type\ContactSearchType
        tags:
            - { name: form.type, alias: Contact_search_type }

9 - Ajout des routes pour la recherche et les résultats

nano src/AddressBook/MainBundle/Resources/config/routing.yml


// ...
search_contact:
    path:     /rechercher
    defaults: { _controller: AddressBookMainBundle:Search:searchContact }
    requirements:
        _method: GET|POST// ...

10 - Créations des vues pour l'affichage du formulaire de recherche et de la liste des résultats

Création du layout :

nano src/AddressBook/MainBundle/Ressources/views/layout.html.twig


{% extends '::base.html.twig' %}

Création de la vue search-contact-form :

nano src/AddressBook/MainBundle/Ressources/views/Search/search-contact-form.html.twig


{% extends 'AddressBookMainBundle::layout.html.twig' %}

{% block body %}
    <form id="search-form-contact" action="{{ path('search_contact') }}">
        {{ form_errors(form) }}
        {{ form_rest(form) }}
    </form>

    <h1>Résultats :</h1>
    <div>
        <table>
            <thead>
            <tr>
                <td>Nom</td>
                <td>Prénom</td>
                <td>Adresse</td>
                <td>Code postal</td>
                <td>Ville</td>
                <td>Tel.</td>
                <td>Email</td>
            </tr>
            </thead>
            <tbody id="results">

            </tbody>
        </table>
    </div>
{%  endblock %}

{% block javascripts %}

    {% javascripts output='js/main-front.js'
    '@AddressBookMainBundle/Resources/public/js/jquery-1.11.1.min.js'
    '@AddressBookMainBundle/Resources/public/js/search.js' %}
    <script src="{{ asset_url }}"></script>

    {% endjavascripts %}

{% endblock %}

Création de la vue result-contact-form :

nano src/AddressBook/MainBundle/Ressources/views/Search/result-contact-form.html.twig


{% for contact in contacts %}
    <tr>
        <td>{{ contact.firstname }}</td>
        <td>{{ contact.lastname }}</td>
        <td>{{ contact.address }}</td>
        <td>{{ contact.zipCode }}</td>
        <td>{{ contact.city }}</td>
        <td>{{ contact.phone }}</td>
        <td>{{ contact.email }}</td>
    </tr>
{% endfor %}

Copie de jQuery : src/AddressBook/MainBundle/Ressources/public/js/jquery-1.11.1.min.js

Création du js executant la call ajax :

nano src/AddressBook/MainBundle/Ressources/public/js/search.js


$(document).ready(function(){

    // SCRIPT

    // listener search button
    $('#search-form-contact').on('submit', function(event){
        event.preventDefault();

        // retrieve form values
        var dataForm = $(this).serialize();

        // todo-pa change ajax route for prod
        // call ajax search
        $.post($(this).attr('action'), dataForm, function(data){

            $('#results').html(data);

            return false;

        });

        return false;

    });

});

Ajout du MainBundle dans la config d'assetic :

nano app/config/config.yml


assetic:
    bundles:        [ AddressBookMainBundle ]

11 - Création de la méthode search du search repository de l'entité Contact

nano src/AddressBook/MainBundle/Entity/SearchRepository/ContactRepository.php


<?php
/**
 * Created by PhpStorm.
 * User: gerard
 * Date: 05/10/2014
 * Time: 20:06
 */

namespace AddressBook\MainBundle\Entity\SearchRepository;

use FOS\ElasticaBundle\Repository;
use AddressBook\MainBundle\Model\ContactSearch;

class ContactRepository extends Repository
{
    public function searchFull(ContactSearch $contactSearch)
    {

        if ($contactSearch->getLastname() == '' && $contactSearch->getFirstname() == '' && $contactSearch->getAddress() == '' && $contactSearch->getPhone() == '' && $contactSearch->getEmail() == '') {
            // query
            $query = new \Elastica\Query\MatchAll();
        } else {
            // query
            $query = new \Elastica\Query\Bool();

            // lastname
            if ($contactSearch->getLastname() != '') {
                $lastnameQuery = new \Elastica\Query\QueryString();
                $lastnameQuery->setFields(array('lastname'));
                $lastnameQuery->setQuery($contactSearch->getLastname());
                $query->addMust($lastnameQuery);
            }

            // firstname
            if ($contactSearch->getFirstname() != '') {
                $firstnameQuery = new \Elastica\Query\QueryString();
                $firstnameQuery->setFields(array('firstname'));
                $firstnameQuery->setQuery($contactSearch->getFirstname());
                $query->addMust($firstnameQuery);
            }

            // address
            if ($contactSearch->getAddress() != '') {
                $addressQuery = new \Elastica\Query\QueryString();
                $addressQuery->setFields(array('address'));
                $addressQuery->setQuery($contactSearch->getAddress());
                $query->addMust($addressQuery);
            }

            // phone
            if ($contactSearch->getPhone() != '') {
                $phoneQuery = new \Elastica\Query\QueryString();
                $phoneQuery->setFields(array('phone'));
                $phoneQuery->setQuery("\"".$contactSearch->getPhone()."\"");
                $query->addMust($phoneQuery);
            }

            // email
            if ($contactSearch->getEmail() != null && $contactSearch->getEmail() != '') {
                $emailQuery = new \Elastica\Query\QueryString();
                $emailQuery->setFields(array('email'));
                $emailQuery->setQuery("\"".$contactSearch->getEmail()."\"");
                $query->addMust($emailQuery);
            }

        }

        return $this->find($query);
    }

}

12 - Création du controller Search nano src/AddressBook/MainBundle/Controller/SearchController.php


<?php
/**
 * Created by PhpStorm.
 * User: gerard
 * Date: 05/10/2014
 * Time: 20:37
 */

namespace AddressBook\MainBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use AddressBook\MainBundle\Model\ContactSearch;
use AddressBook\MainBundle\Form\Type\ContactSearchType;
use Symfony\Component\HttpFoundation\Response;

class SearchController extends Controller
{
    public function searchContactAction()
    {

        // create Contact model
        $ContactSearch = new ContactSearch();

        // create form
        $ContactSearchForm = $this->createForm(
            new ContactSearchType(),
            $ContactSearch
        );
        //die('coucou');

        // check if is ajax request
        if ($this->getRequest()->isXmlHttpRequest()) {

            // bind data
            $ContactSearchForm->handleRequest($this->getRequest());

            // get form data
            $ContactSearch = $ContactSearchForm->getData();

            // call elastic manager
            $elasticManager = $this->container->get('fos_elastica.manager.orm');

            // retrieve results
            $results = $elasticManager->getRepository('AddressBookMainBundle:Contact')->searchFull($ContactSearch);

            // send response
            return $this->render('AddressBookMainBundle:Search:result-contact-form.html.twig', array(
                'contacts' => $results
            ));
        }

        // send view
        return $this->render('AddressBookMainBundle:Search:search-contact-form.html.twig', array(
            'form' => $ContactSearchForm->createView()
        ));
    }
}

Peupler l'index d'elasticseach :

php app/console fos:elastica:populate

Source : Obtao.com