Symfony2 - Jobeet - Jour 15 - Services Web

1 1 1 1 1 1 1 1 1 1 Rating 0.00 (0 Votes)
Submit to DeliciousSubmit to DiggSubmit to FacebookSubmit to Google PlusSubmit to StumbleuponSubmit to TechnoratiSubmit to TwitterSubmit to LinkedIn

Avec l'ajout de flux sur Jobeet, les chercheurs d'emploi peuvent désormais être informés des nouveaux emplois en temps réel.

De l'autre côté de la barrière, lorsque vous postez un emploi, vous voulez avoir la plus grande exposition possible. Si votre emploi est diffusé sur beaucoup de petits sites Web, vous aurez une meilleure chance de trouver la bonne personne. Les affiliés seront en mesure de publier les dernières offres d'emploi sur leurs sites Web grâce aux services web que nous allons développer aujourd'hui.

Affiliés

Comme nous l'avons déjà dit dans le jour 2 de ce tutoriel (Symfony2 - Jobeet - Jour 02 - Le projet), un affilié récupère la liste actuelle des emplois actifs.

Les fixtures

Créons un nouveau fichier de fixation pour les affiliés :

  • src/Erlem/JobeetBundle/DataFixtures/ORM/LoadAffiliateData.php
<?php
 
namespace Erlem\JobeetBundle\DataFixtures\ORM;
 
use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\Common\DataFixtures\AbstractFixture;
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
use Erlem\JobeetBundle\Entity\Affiliate;
 
class LoadAffiliateData extends AbstractFixture implements OrderedFixtureInterface
{
    public function load(ObjectManager $em)
    {
        $affiliate = new Affiliate();
 
        $affiliate->setUrl('http://sensio-labs.com/');
        $affiliate->setEmail('address1[at]example.com');
        $affiliate->setToken('sensio-labs');
        $affiliate->setIsActive(true);
        $affiliate->addCategory($em->merge($this->getReference('category-programming')));
 
        $em->persist($affiliate);
 
        $affiliate = new Affiliate();
 
        $affiliate->setUrl('/');
        $affiliate->setEmail('address2[at]example.org');
        $affiliate->setToken('symfony');
        $affiliate->setIsActive(false);
        $affiliate->addCategory($em->merge($this->getReference('category-programming')), $em->merge($this->getReference('category-design')));
 
        $em->persist($affiliate);
        $em->flush();
 
        $this->addReference('affiliate', $affiliate);
    }
 
    public function getOrder()
    {
        return 3; // This represents the order in which fixtures will be loaded
    }
}

Maintenant, pour envoyer les données en base de données, il suffit d'exécuter la commande suivante :

php app/console doctrine:fixtures:load

Dans le fichier de fixures, les jetons sont codés en dur pour simplifier le test, mais quand un utilisateur réel demande un compte, le jeton devra être généré. Créons une fonction pour le faire dans notre classe d'affiliation. Commencez par ajouter la méthode de setTokenValue à lifecycleCallbacks :

  • src/Erlem/JobeetBundle/Resources/config/doctrine/Affiliate.orm.ym
# ... 
    lifecycleCallbacks:
        prePersist: [ setCreatedAtValue, setTokenValue ]

Maintenant, la méthode setTokenValue sera générée dans le fichier de l'entité lorsque vous exécutez la commande suivante :

php app/console doctrine:generate:entities ErlemJobeetBundle

Essayons de modifier la méthode maintenant :

  • src/Erlem/JobeetBundle/Entity/Affiliate.php
    public function setTokenValue()
    {
        if(!$this->getToken()) {
            $token = sha1($this->getEmail().rand(11111, 99999));
            $this->token = $token;
        }

        return $this;
    }

Recharger les données :

php app/console doctrine:fixtures:load

Le service Web Job

Comme toujours, lorsque vous créez une nouvelle ressource, c'est une bonne habitude de définir en premier la route :

  • src/Erlem/JobeetBundle/Resources/config/routing.yml
# ...

ErlemJobeetBundle_api:
    pattern: /api/{token}/jobs.{_format}
    defaults: {_controller: "ErlemJobeetBundle:Api:list"}
    requirements:
        _format: xml|json|yaml

Comme d'habitude, une fois que vous modifiez un fichier de routage, vous devez effacer le cache :

php app/console cache:clear --env=dev
php app/console cache:clear --env=prod

La prochaine étape est de créer l'action api et les templates, qui partagent la même action. Créons maintenant un nouveau fichier contrôleur, appelé ApiController :

  • src/Erlem/JobeetBundle/Controller/ApiController.php
<?php

namespace Erlem\JobeetBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Erlem\JobeetBundle\Entity\Affiliate;
use Erlem\JobeetBundle\Entity\Job;
use Erlem\JobeetBundle\Repository\AffiliateRepository;

class ApiController extends Controller
{
    public function listAction(Request $request, $token)
    {
        $em = $this->getDoctrine()->getManager();

        $jobs = array();

        $rep = $em->getRepository('ErlemJobeetBundle:Affiliate');
        $affiliate = $rep->getForToken($token);

        if(!$affiliate) { 
            throw $this->createNotFoundException('This affiliate account does not exist!');
        }

        $rep = $em->getRepository('ErlemJobeetBundle:Job');
        $active_jobs = $rep->getActiveJobs(null, null, null, $affiliate->getId());

        foreach ($active_jobs as $job) {
            $jobs[$this->get('router')->generate('erlem_job_show', array('company' => $job->getCompanySlug(), 'location' => $job->getLocationSlug(), 'id' => $job->getId(), 'position' => $job->getPositionSlug()), true)] = $job->asArray($request->getHost());
        }

        $format = $request->getRequestFormat();
        $jsonData = json_encode($jobs);

        if ($format == "json") {
            $headers = array('Content-Type' => 'application/json'); 
            $response = new Response($jsonData, 200, $headers);

            return $response;
        }

        return $this->render('ErlemJobeetBundle:Api:jobs.' . $format . '.twig', array('jobs' => $jobs));  
    }
}

Pour récupérer l'affiliation en utilisant son jeton, nous allons créer la méthode getForToken(). Cette méthode permet également de vérifier si le compte d'affilié est activé, il n'est donc pas nécessaire pour nous de vérifier une fois de plus. Jusqu'à présent, nous n'avons pas encore utilisé AffiliateRepository, de sorte qu'elle n'existe pas. Pour la créer, modifier le fichier ORM comme suit, puis exécutez la commande que vous avez utilisé avant de générer les entités.

  • src/Erlem/JobeetBundle/Resources/config/doctrine/Affiliate.orm.yml
Erlem\JobeetBundle\Entity\Affiliate:
    type: entity
    repositoryClass: Erlem\JobeetBundle\Repository\AffiliateRepository
    # ...

Une fois créé, il est prêt à être utilisé :

  • src/Erlem/JobeetBundle/Repository/AffiliateRepository.php
<?php

namespace Erlem\JobeetBundle\Repository;

use Doctrine\ORM\EntityRepository;

/**
 * AffiliateRepository
 *
 * This class was generated by the Doctrine ORM. Add your own custom
 * repository methods below.
 */
class AffiliateRepository extends EntityRepository
{
    public function getForToken($token)
    {
        $qb = $this->createQueryBuilder('a')
            ->where('a.is_active = :active')
            ->setParameter('active', 1)
            ->andWhere('a.token = :token')
            ->setParameter('token', $token)
            ->setMaxResults(1)
        ;

        try{
            $affiliate = $qb->getQuery()->getSingleResult();
        } catch(\Doctrine\Orm\NoResultException $e){
            $affiliate = null;
        }

        return $affiliate;
    }
}

Après identification de l'affilié par son jeton, nous allons utiliser la méthode getActiveJobs() pour donner à l'affilié les emplois dont il avait besoin, appartenant aux catégories sélectionnées. Si vous ouvrez votre fichier JobRepository maintenant, vous verrez que la méthode getActiveJobs() ne partage aucune relation avec les affiliés. Parce que nous voulons réutiliser cette méthode, nous avons besoin de faire quelques modifications à l'intérieur de celle-ci :

  • src/Erlem/JobeetBundle/Repository/JobRepository.php
// ...

    public function getActiveJobs($category_id = null, $max = null, $offset = null, $affiliate_id = null)
    {
        $qb = $this->createQueryBuilder('j')
            ->where('j.expires_at > :date')
            ->setParameter('date', date('Y-m-d H:i:s', time()))
            ->andWhere('j.is_activated = :activated')
            ->setParameter('activated', 1)
            ->orderBy('j.expires_at', 'DESC');

        if($max) {
            $qb->setMaxResults($max);
        }

        if($offset) {
            $qb->setFirstResult($offset);
        }

        if($category_id) {
            $qb->andWhere('j.category = :category_id')
                ->setParameter('category_id', $category_id);
        }
        // j.category c, c.affiliate a
        if($affiliate_id) {
            $qb->leftJoin('j.category', 'c')
               ->leftJoin('c.affiliates', 'a')
               ->andWhere('a.id = :affiliate_id')
               ->setParameter('affiliate_id', $affiliate_id)
            ;
        }

        $query = $qb->getQuery();

        return $query->getResult();
    }

// ...

Comme vous pouvez le voir, nous remplissons le tableau de Job d'une fonction appelée asArray(). Définissons cette méthode :

  • src/Erlem/JobeetBundle/Entity/Job.php
public function asArray($host)
{
    return array(
        'category'     => $this->getCategory()->getName(),
        'type'         => $this->getType(),
        'company'      => $this->getCompany(),
        'logo'         => $this->getLogo() ? 'http://' . $host . '/uploads/jobs/' . $this->getLogo() : null,
        'url'          => $this->getUrl(),
        'position'     => $this->getPosition(),
        'location'     => $this->getLocation(),
        'description'  => $this->getDescription(),
        'how_to_apply' => $this->getHowToApply(),
        'expires_at'   => $this->getCreatedAt()->format('Y-m-d H:i:s'),
    );
}

Le format XML

Utiliser le format xml est aussi simple que de créer un template :

  • src/Erlem/JobeetBundle/Resources/views/Api/jobs.xml.twig
<?xml version="1.0" encoding="utf-8"?>
<jobs>
{% for url, job in jobs %}
    <job url={{ url }}>
{% for key,value in job %}
        <{{ key }}>{{ value }}</{{ key }}>
{% endfor %}
    </job>
{% endfor %}
</jobs>

Le format JSON

Utiliser le format JSON est similaire :

  • src/Erlem/JobeetBundle/Resources/views/Api/jobs.json.twig
{% for url, job in jobs %}
{% i = 0, count(jobs), ++i %}
[
    "url":{{ url }},
{% for key, value in job %} {% j = 0, count(key), ++j %}
    {{ key }}:"{% if j == count(key)%} {{ json_encode(value) }}, {% else %} {{ json_encode(value) }}
                 {% endif %}"
{% endfor %}]
{% endfor %}

Le format YAML

  • src/Erlem/JobeetBundle/Resources/views/Api/jobs.yaml.twig
{% for url,job in jobs %}
    Url: {{ url }}
{% for key, value in job %}
        {{ key }}: {{ value }}
{% endfor %}
{% endfor %}

Si vous essayez d'appeler le service web avec un jeton non-valide, vous recevrez une page 404 comme une réponse, pour tous les formats.

Pour voir ce que vous avez accompli jusqu'à présent, accédez aux liens suivants :

Changez l'extension dans l'URL, selon le format que vous préférez. Exemple avec le format XML.

01-114-symfony2-jobeet-jour-15-services-web

Tests de service Web

  •  src/Erlem/JobeetBundle/Tests/Controller/ApiControllerTest.php
<?php

namespace Erlem\JobeetBundle\Tests\Controller;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\Console\Input\ArrayInput;
use Doctrine\Bundle\DoctrineBundle\Command\DropDatabaseDoctrineCommand;
use Doctrine\Bundle\DoctrineBundle\Command\CreateDatabaseDoctrineCommand;
use Doctrine\Bundle\DoctrineBundle\Command\Proxy\CreateSchemaDoctrineCommand;
use Symfony\Component\DomCrawler\Crawler;
use Symfony\Component\HttpFoundation\HttpExceptionInterface;

class ApiControllerTest extends WebTestCase
{
    private $em;

    private $application;

    public function setUp()
    {
        static::$kernel = static::createKernel();
        static::$kernel->boot();

        $this->application = new Application(static::$kernel);

        // drop the database
        $command = new DropDatabaseDoctrineCommand();
        $this->application->add($command);
        $input = new ArrayInput(array(
            'command' => 'doctrine:database:drop',
            '--force' => true
        ));
        $command->run($input, new NullOutput());

        // we have to close the connection after dropping the database so we don't get "No database selected" error
        $connection = $this->application->getKernel()->getContainer()->get('doctrine')->getConnection();
        if ($connection->isConnected()) {
            $connection->close();
        }

        // create the database
        $command = new CreateDatabaseDoctrineCommand();
        $this->application->add($command);
        $input = new ArrayInput(array(
            'command' => 'doctrine:database:create',
        ));
        $command->run($input, new NullOutput());

        // create schema
        $command = new CreateSchemaDoctrineCommand();
        $this->application->add($command);
        $input = new ArrayInput(array(
            'command' => 'doctrine:schema:create',
        ));
        $command->run($input, new NullOutput());

        // get the Entity Manager
        $this->em = static::$kernel->getContainer()
            ->get('doctrine')
            ->getManager();

        // load fixtures
        $client = static::createClient();
        $loader = new \Symfony\Bridge\Doctrine\DataFixtures\ContainerAwareLoader($client->getContainer());
        $loader->loadFromDirectory(static::$kernel->locateResource('@ErlemJobeetBundle/DataFixtures/ORM'));
        $purger = new \Doctrine\Common\DataFixtures\Purger\ORMPurger($this->em);
        $executor = new \Doctrine\Common\DataFixtures\Executor\ORMExecutor($this->em, $purger);
        $executor->execute($loader->getFixtures());
    }

    public function testList()
    {
        $client = static::createClient();
        $crawler = $client->request('GET', '/api/sensio-labs/jobs.xml');

        $this->assertEquals('Erlem\JobeetBundle\Controller\ApiController::listAction', $client->getRequest()->attributes->get('_controller'));
        $this->assertTrue($crawler->filter('description')->count() == 32);

        $crawler = $client->request('GET', '/api/sensio-labs87/jobs.xml');

        $this->assertTrue(404 === $client->getResponse()->getStatusCode());

        $crawler = $client->request('GET', '/api/symfony/jobs.xml');

        $this->assertTrue(404 === $client->getResponse()->getStatusCode());

        $crawler = $client->request('GET', '/api/sensio-labs/jobs.json');

        $this->assertEquals('Erlem\JobeetBundle\Controller\ApiController::listAction', $client->getRequest()->attributes->get('_controller'));
        $this->assertRegExp('/"category"\:"Programming"/', $client->getResponse()->getContent());

        $crawler = $client->request('GET', '/api/sensio-labs87/jobs.json');

        $this->assertTrue(404 === $client->getResponse()->getStatusCode());

        $crawler = $client->request('GET', '/api/sensio-labs/jobs.yaml');
        $this->assertRegExp('/category\: Programming/', $client->getResponse()->getContent());

        $this->assertEquals('Erlem\JobeetBundle\Controller\ApiController::listAction', $client->getRequest()->attributes->get('_controller'));

        $crawler = $client->request('GET', '/api/sensio-labs87/jobs.yaml');

        $this->assertTrue(404 === $client->getResponse()->getStatusCode());
    }
}

Dans le fichier de ApiControllerTest, nous testons que les formats demandés sont correctement reçus et les pages demandées sont correctement restitués.

Pour lancer le test :

phpunit -c app/ src/Erlem/JobeetBundle/Tests/Controller/ApiControllerTest

Le résultat :

PHPUnit 4.0.17 by Sebastian Bergmann.

.

Time: 8.8 seconds, Memory: 69.50Mb

OK (1 test, 10 assertions)

Le formulaire de demande d'affiliation

Maintenant que le service web est prêt à être utilisé, nous allons créer le formulaire de création d'affiliés. Pour cela, vous devez écrire le formulaire HTML, implémenter des règles de validation pour chaque champ, traiter les valeurs pour les stocker dans une base de données, messages d'erreur d'affichage et de repeupler les champs en cas d'erreurs.

Tout d'abord, créer un fichier nouveau contrôleur, nommé AffiliateController :

  • src/Erlem/JobeetBundle/Controller/AffiliateController.php
<?php

namespace Erlem\JobeetBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Erlem\JobeetBundle\Entity\Affiliate;
use Erlem\JobeetBundle\Form\AffiliateType;
use Symfony\Component\HttpFoundation\Request;
use Erlem\JobeetBundle\Entity\Category;

class AffiliateController extends Controller
{
    // Your code goes here
}

Ensuite, modifiez le lien Affiliés :

  • src/Erlem/JobeetBundle/Resources/views/layout.html.twig
<!-- ... -->
    <li class="last"><a href={{ path('erlem_affiliate_new') }}>Become an affiliate</a></li>
<!-- ... -->

Maintenant, nous devons créer une action en fonction de la voie à partir du lien que vous venez de modifier plus tôt :

  • src/Erlem/JobeetBundle/Controller/AffiliateController.php
<?php

namespace Erlem\JobeetBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Erlem\JobeetBundle\Entity\Affiliate;
use Erlem\JobeetBundle\Form\AffiliateType;
use Symfony\Component\HttpFoundation\Request;
use Erlem\JobeetBundle\Entity\Category;

class AffiliateController extends Controller
{
    public function newAction()
    {
        $entity = new Affiliate();
        $form = $this->createForm(new AffiliateType(), $entity);

        return $this->render('ErlemJobeetBundle:Affiliate:affiliate_new.html.twig', array(
            'entity' => $entity,
            'form'   => $form->createView(),
        ));
    }
}

Nous avons le nom de la route, nous avons l'action, mais nous n'avons pas la route, nous allons donc créer :

  • src/Erlem/JobeetBundle/Resources/config/routing/affiliate.yml
erlem_affiliate_new:
    pattern:  /new
    defaults: { _controller: "ErlemJobeetBundle:Affiliate:new" }

En outre, ajouter ceci à votre fichier de configuration :

  • src/Erlem/JobeetBundle/Resources/config/routing.yml
# ...

ErlemJobeetBundle_erlem_affiliate:
    resource: "@ErlemJobeetBundle/Resources/config/routing/affiliate.yml"
    prefix:   /affiliate

Le formulaire doit également être créé. Mais, même si l'affiliation a plus de champs, nous ne les afficherons pas tous, parce que certains d'entre eux ne doit pas être modifiable par l'utilisateur final. Créez votre formulaire d'affiliation :

  • src/Erlem/JobeetBundle/Form/AffiliateType.php
<?php

namespace Erlem\JobeetBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Erlem\JobeetBundle\Entity\Affiliate;
use Erlem\JobeetBundle\Entity\Category;

class AffiliateType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('url')
            ->add('email')
            ->add('categories', null, array('expanded'=>true))
        ;
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'Erlem\JobeetBundle\Entity\Affiliate',
        ));
    }

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

Maintenant, nous devons décider si oui ou non l'objet d'affiliation est valable après la forme a appliqué les données qui lui sont soumises. Pour ce faire, ajoutez le code suivant à votre fichier de validation :

  • src/Erlem/JobeetBundle/Resources/config/validation.yml
# ...

Erlem\JobeetBundle\Entity\Affiliate:
    constraints:
        - Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity: email
    properties:
        url:
            - Url: ~
        email:
            - NotBlank: ~
            - Email: ~

Dans le schéma de validation, nous avons utilisé une nouvelle validation, appelé UniqueEntity. Elle confirme un champs particulier dans une entité Doctrine unique. Ceci est communément utilisé, par exemple, pour éviter un nouvel utilisateur à enregistrer à l'aide d'une adresse électronique qui existe déjà dans le système.

N'oubliez pas de vider votre cache après l'application des contraintes de validation !

Enfin, nous allons créer la vue :

  • src/Erlem/JobeetBundle/Resources/views/Affiliate/affiliate_new.html.twig
{% extends 'ErlemJobeetBundle::layout.html.twig' %}

{% set form_themes = _self %}

{% block form_errors %}
{% spaceless %}
    {% if errors|length > 0 %}
        <ul class="error_list">
            {% for error in errors %}
                <li>{{ error.messageTemplate|trans(error.messageParameters, 'validators') }}</li>
            {% endfor %}
        </ul>
    {% endif %}
{% endspaceless %}
{% endblock form_errors %}

{% block stylesheets %}
    {{ parent() }}
    <link rel="stylesheet" href={{ asset('bundles/erlemjobeet/css/job.css') }} type="text/css" media="all" />
{% endblock %}

{% block content %}
    <h1>Become an affiliate</h1>
        <form action={{ path('erlem_affiliate_create') }} method="post" {{ form_enctype(form) }}>
            <table id="job_form">
                <tfoot>
                    <tr>
                        <td colspan="2">
                            <input type="submit" value="Submit" />
                        </td>
                    </tr>
                </tfoot>
                <tbody>
                    <tr>
                        <th>{{ form_label(form.url) }}</th>
                        <td>
                            {{ form_errors(form.url) }}
                            {{ form_widget(form.url) }}
                        </td>
                    </tr>
                    <tr>
                        <th>{{ form_label(form.email) }}</th>
                        <td>
                            {{ form_errors(form.email) }}
                            {{ form_widget(form.email) }}
                        </td>
                    </tr>
                    <tr>
                        <th>{{ form_label(form.categories) }}</th>
                        <td>
                            {{ form_errors(form.categories) }}
                            {{ form_widget(form.categories) }}
                        </td>
                    </tr>
                </tbody>
            </table>
        {{ form_end(form) }}
{% endblock %}

Lorsque l'utilisateur soumet un formulaire, les données du formulaire doivent être conservées dans la base de données, si elle est valide. Ajouter le nouveau créer l'action de votre contrôleur d'affiliation:

  • src/Erlem/JobeetBundle/Controller/AffiliateController.php
class AffiliateController extends Controller 
{
    // ...    

    public function createAction(Request $request)
    {
        $affiliate = new Affiliate();
        $form = $this->createForm(new AffiliateType(), $affiliate);
        $form->bind($request);
        $em = $this->getDoctrine()->getManager();

        if ($form->isValid()) {

            $formData = $request->get('affiliate');
            $affiliate->setUrl($formData['url']);
            $affiliate->setEmail($formData['email']);
            $affiliate->setIsActive(false);

            $em->persist($affiliate);
            $em->flush();

            return $this->redirect($this->generateUrl('erlem_affiliate_wait'));
        }

        return $this->render('ErlemJobeetBundle:Affiliate:affiliate_new.html.twig', array(
            'entity' => $affiliate,
            'form'   => $form->createView(),
        ));
    }
}

Lors de la présentation, l'action de créer est effectuée, donc nous avons besoin de définir la route :

  •  src/Erlem/JobeetBundle/Resources/config/routing/affiliate.yml
# ...

erlem_affiliate_create:
    pattern: /create
    defaults: { _controller: "ErlemJobeetBundle:Affiliate:create" }
    requirements: { _method: post }

Ensuite, l'utilisateur est redirigé vers une page d'attente. Nous allons définir l'action et créer la vue :

  •  src/Erlem/JobeetBundle/Controller/AffiliateController.php
class AffiliateController extends Controller
{
    // ...

    public function waitAction()
    {
        return $this->render('ErlemJobeetBundle:Affiliate:wait.html.twig');
    }
}
  •  src/Erlem/JobeetBundle/Resources/views/Affiliate/wait.html.twig
{% extends "ErlemJobeetBundle::layout.html.twig" %}

{% block content %}
    <div class="content">
        <h1>Your affiliate account has been created</h1>
        <div style="padding: 20px">
            Thank you!
            You will receive an email with your affiliate token
            as soon as your account will be activated.
        </div>
    </div>
{% endblock %}

Maintenant, la route :

  • src/Erlem/JobeetBundle/Resources/config/routing/affiliate.yml
# ...

erlem_affiliate_wait:
    pattern: /wait
    defaults: { _controller: "ErlemJobeetBundle:Affiliate:wait" }

Après avoir défini les routes, afin de travailler, vous devez effacer le cache.

php app/console cache:clear --env=dev
php app/console cache:clear --env=prod

Maintenant, si vous cliquez sur le lien Affiliés sur la page d'accueil, vous serez dirigé vers la page de formulaire d'affiliation.

02-114-symfony2-jobeet-jour-15-services-web

03-114-symfony2-jobeet-jour-15-services-web

Tests

 La dernière étape consiste à écrire des tests fonctionnels pour la nouvelle fonctionnalité.

  •  src/Erlem/JobeetBundle/Tests/Controller/AffiliateControllerTest.php
<?php

namespace Erlem\JobeetBundle\Tests\Controller;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\Console\Input\ArrayInput;
use Doctrine\Bundle\DoctrineBundle\Command\DropDatabaseDoctrineCommand;
use Doctrine\Bundle\DoctrineBundle\Command\CreateDatabaseDoctrineCommand;
use Doctrine\Bundle\DoctrineBundle\Command\Proxy\CreateSchemaDoctrineCommand;
use Symfony\Component\DomCrawler\Crawler;

class AffiliateControllerTest extends WebTestCase
{
    private $em;
    private $application;

    public function setUp()
    {
        static::$kernel = static::createKernel();
        static::$kernel->boot();

        $this->application = new Application(static::$kernel);

        // drop the database
        $command = new DropDatabaseDoctrineCommand();
        $this->application->add($command);
        $input = new ArrayInput(array(
            'command' => 'doctrine:database:drop',
            '--force' => true
        ));
        $command->run($input, new NullOutput());

        // we have to close the connection after dropping the database so we don't get "No database selected" error
        $connection = $this->application->getKernel()->getContainer()->get('doctrine')->getConnection();
        if ($connection->isConnected()) {
            $connection->close();
        }

        // create the database
        $command = new CreateDatabaseDoctrineCommand();
        $this->application->add($command);
        $input = new ArrayInput(array(
            'command' => 'doctrine:database:create',
        ));
        $command->run($input, new NullOutput());

        // create schema
        $command = new CreateSchemaDoctrineCommand();
        $this->application->add($command);
        $input = new ArrayInput(array(
            'command' => 'doctrine:schema:create',
        ));
        $command->run($input, new NullOutput());

        // get the Entity Manager
        $this->em = static::$kernel->getContainer()
            ->get('doctrine')
            ->getManager();

        // load fixtures
        $client = static::createClient();
        $loader = new \Symfony\Bridge\Doctrine\DataFixtures\ContainerAwareLoader($client->getContainer());
        $loader->loadFromDirectory(static::$kernel->locateResource('@ErlemJobeetBundle/DataFixtures/ORM'));
        $purger = new \Doctrine\Common\DataFixtures\Purger\ORMPurger($this->em);
        $executor = new \Doctrine\Common\DataFixtures\Executor\ORMExecutor($this->em, $purger);
        $executor->execute($loader->getFixtures());
    }

    public function testAffiliateForm()
    {
        $client = static::createClient();
        $crawler = $client->request('GET', '/affiliate/new');

        $this->assertEquals('Erlem\JobeetBundle\Controller\AffiliateController::newAction', $client->getRequest()->attributes->get('_controller'));

        $form = $crawler->selectButton('Submit')->form(array(
            'affiliate[url]' => 'http://sensio-labs.com/',
            'affiliate[email]' => 'jobeet[at]example.com'
        ));

        $client->submit($form);
        $this->assertEquals('Erlem\JobeetBundle\Controller\AffiliateController::createAction', $client->getRequest()->attributes->get('_controller'));

        $kernel = static::createKernel();
        $kernel->boot();
        $em = $kernel->getContainer()->get('doctrine.orm.entity_manager');

        $query = $em->createQuery('SELECT count(a.email) FROM ErlemJobeetBundle:Affiliate a WHERE a.email = :email');
        $query->setParameter('email', 'jobeet[at]example.com');
        $this->assertEquals(1, $query->getSingleScalarResult());

        $crawler = $client->request('GET', '/affiliate/new');
        $form = $crawler->selectButton('Submit')->form(array(
            'affiliate[email]'        => 'not.an.email',
        ));
        $crawler = $client->submit($form);

        // check if we have 1 errors
        $this->assertTrue($crawler->filter('.error_list')->count() == 1);
        // check if we have error on affiliate_email field
        $this->assertTrue($crawler->filter('#affiliate_email')->siblings()->first()->filter('.error_list')->count() == 1);
    }

    public function testCreate()
    {
        $client = static::createClient();
        $crawler = $client->request('GET', '/affiliate/new');
        $form = $crawler->selectButton('Submit')->form(array(
            'affiliate[url]' => 'http://sensio-labs.com/',
            'affiliate[email]' => 'address[at]example.com'
        ));

        $client->submit($form);
        $client->followRedirect();

        $this->assertEquals('Erlem\JobeetBundle\Controller\AffiliateController::waitAction', $client->getRequest()->attributes->get('_controller'));

        return $client;
    }

    public function testWait()
    {
        $client = static::createClient();
        $crawler = $client->request('GET', '/affiliate/wait');

        $this->assertEquals('Erlem\JobeetBundle\Controller\AffiliateController::waitAction', $client->getRequest()->attributes->get('_controller'));
    }
}

Pour lancer le test :

phpunit -c app/ src/Erlem/JobeetBundle/Tests/Controller/AffiliateControllerTest

L'Affilié Backend

Pour le backend, nous allons travailler avec SonataAdminBundle. Comme nous l'avons dit, après s'être enregistré, l'affilié doit attendre pour que l'administrateur active son compte. Ainsi, lorsque l'administrateur aura accès à la page d'affiliés, il ne verra que les comptes inactivés, pour l'aider à être plus productif.
Tout d'abord, vous devez déclarer le nouveau service d'affiliation dans votre fichier services.yml :

  • src/Erlem/JobeetBundle/Resources/config/services.yml
# ...
    erlem.jobeet.admin.affiliate:
        class: Erlem\JobeetBundle\Admin\AffiliateAdmin
        tags:
            - { name: sonata.admin, manager_type: orm, group: jobeet, label: Affiliates }
        arguments:
            - ~
            - Erlem\JobeetBundle\Entity\Affiliate
            - 'ErlemJobeetBundle:AffiliateAdmin'

Après cela, créez le fichier Admin :

  • src/Erlem/JobeetBundle/Admin/AffiliateAdmin.php
<?php

namespace Erlem\JobeetBundle\Admin;

use Sonata\AdminBundle\Admin\Admin;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Validator\ErrorElement;
use Sonata\AdminBundle\Form\FormMapper;
use Sonata\AdminBundle\Show\ShowMapper;
use Erlem\JobeetBundle\Entity\Affiliate;

class AffiliateAdmin extends Admin
{
    protected $datagridValues = array(
        '_sort_order' => 'ASC',
        '_sort_by' => 'is_active'
    );

    protected function configureFormFields(FormMapper $formMapper)
    {
        $formMapper
            ->add('email')
            ->add('url')
        ;
    }

    protected function configureDatagridFilters(DatagridMapper $datagridMapper)
    {
        $datagridMapper
            ->add('email')
            ->add('is_active');
    }

    protected function configureListFields(ListMapper $listMapper)
    {
        $listMapper
            ->add('is_active')
            ->addIdentifier('email')
            ->add('url')
            ->add('created_at')
            ->add('token')
        ;
    }
}

Pour aider l'administrateur, nous voulons afficher uniquement les comptes inactivés. Ceci peut être fait en réglant le filtre 'is_active' false :

  • src/Erlem/JobeetBundle/Admin/AffiliateAdmin.php
// ...
    protected $datagridValues = array(
        '_sort_order' => 'ASC',
        '_sort_by' => 'is_active',
        'is_active' => array('value' => 2) // The value 2 represents that the displayed affiliate accounts are not activated yet
    );

// ...

Maintenant, créez le fichier du contrôleur de AffiliateAdmin :

  • src/Erlem/JobeetBundle/Controller/AffiliateAdminController.php
<?php

namespace Erlem\JobeetBundle\Controller;

use Sonata\AdminBundle\Controller\CRUDController as Controller;
use Sonata\DoctrineORMAdminBundle\Datagrid\ProxyQuery as ProxyQueryInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;

class AffiliateAdminController extends Controller
{
    // Your code goes here
}

Créons l'activation et la désactivation l'action batch :

  •  src/Erlem/JobeetBundle/Controller/AffiliateAdminController.php
<?php

namespace Erlem\JobeetBundle\Controller;

use Sonata\AdminBundle\Controller\CRUDController as Controller;
use Sonata\DoctrineORMAdminBundle\Datagrid\ProxyQuery as ProxyQueryInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;

class AffiliateAdminController extends Controller
{
    public function batchActionActivate(ProxyQueryInterface $selectedModelQuery)
    {
        if($this->admin->isGranted('EDIT') === false || $this->admin->isGranted('DELETE') === false) {
            throw new AccessDeniedException();
        }

        $request = $this->get('request');
        $modelManager = $this->admin->getModelManager();

        $selectedModels = $selectedModelQuery->execute();

        try {
            foreach($selectedModels as $selectedModel) {
                $selectedModel->activate();
                $modelManager->update($selectedModel);
            }
        } catch(\Exception $e) {
            $this->get('session')->getFlashBag()->add('sonata_flash_error', $e->getMessage());

            return new RedirectResponse($this->admin->generateUrl('list',$this->admin->getFilterParameters()));
        }

        $this->get('session')->getFlashBag()->add('sonata_flash_success',  sprintf('The selected accounts have been activated'));

        return new RedirectResponse($this->admin->generateUrl('list',$this->admin->getFilterParameters()));
    }

    public function batchActionDeactivate(ProxyQueryInterface $selectedModelQuery)
    {
        if($this->admin->isGranted('EDIT') === false || $this->admin->isGranted('DELETE') === false) {
            throw new AccessDeniedException();
        }

        $request = $this->get('request');
        $modelManager = $this->admin->getModelManager();

        $selectedModels = $selectedModelQuery->execute();

        try {
            foreach($selectedModels as $selectedModel) {
                $selectedModel->deactivate();
                $modelManager->update($selectedModel);
            }
        } catch(\Exception $e) {
            $this->get('session')->getFlashBag()->add('sonata_flash_error', $e->getMessage());

            return new RedirectResponse($this->admin->generateUrl('list',$this->admin->getFilterParameters()));
        }

        $this->get('session')->getFlashBag()->add('sonata_flash_success',  sprintf('The selected accounts have been deactivated'));

        return new RedirectResponse($this->admin->generateUrl('list',$this->admin->getFilterParameters()));
    }
}

Pour que les nouvelles actions de commandes soient fonctionnelles, il faut les ajouter dans getBatchActions de la classe Admin :

  • src/Erlem/JobeetBundle/Admin/AffiliateAdmin.php
class AffiliateAdmin extends Admin
{
    // ... 

    public function getBatchActions()
    {
        $actions = parent::getBatchActions();

        if($this->hasRoute('edit') && $this->isGranted('EDIT') && $this->hasRoute('delete') && $this->isGranted('DELETE')) {
            $actions['activate'] = array(
                'label'            => 'Activate',
                'ask_confirmation' => true
            );

            $actions['deactivate'] = array(
                'label'            => 'Deactivate',
                'ask_confirmation' => true
            );
        }

        return $actions;
    }
}

Pour que cela fonctionne, vous devez ajouter les deux méthodes, activer et désactiver, dans le fichier de l'entité :

  • src/Erlem/JobeetBundle/Entity/Affiliate.php
// ...

    public function activate()
    {
        if(!$this->getIsActive()) {
            $this->setIsActive(true);
        }

        return $this->is_active;
    }

    public function deactivate()
    {
        if($this->getIsActive()) {
            $this->setIsActive(false);
        }

        return $this->is_active;
    }

Nous allons maintenant créer deux actions individuelles, activer et désactiver, pour chaque élément. Tout d'abord, nous allons créer les routes. C'est pourquoi, dans votre classe Admin, vous prolongerez la fonction configureRoutes :

  • src/Erlem/JobeetBundle/Admin/AffiliateAdmin.php
use Sonata\AdminBundle\Route\RouteCollection;

class AffiliateAdmin extends Admin
{
    // ...

    protected function configureRoutes(RouteCollection $collection) {
        parent::configureRoutes($collection);

        $collection->add('activate',
            $this->getRouterIdParameter().'/activate')
        ;

        $collection->add('deactivate',
            $this->getRouterIdParameter().'/deactivate')
        ;
    }
}

Il est temps de mettre en œuvre les actions dans le AdminController :

  • src/Erlem/JobeetBundle/Controller/AffiliateAdminController.php
class AffiliateAdminController extends Controller
{
    // ...

    public function activateAction($id)
    {
        if($this->admin->isGranted('EDIT') === false) {
            throw new AccessDeniedException();
        }

        $em = $this->getDoctrine()->getManager();
        $affiliate = $em->getRepository('ErlemJobeetBundle:Affiliate')->findOneById($id);

        try {
            $affiliate->setIsActive(true);
            $em->flush();
        } catch(\Exception $e) {
            $this->get('session')->getFlashBag()->add('sonata_flash_error', $e->getMessage());

            return new RedirectResponse($this->admin->generateUrl('list', $this->admin->getFilterParameters()));
        }

        return new RedirectResponse($this->admin->generateUrl('list',$this->admin->getFilterParameters()));

    }

    public function deactivateAction($id)
    {
        if($this->admin->isGranted('EDIT') === false) {
            throw new AccessDeniedException();
        }

        $em = $this->getDoctrine()->getManager();
        $affiliate = $em->getRepository('ErlemJobeetBundle:Affiliate')->findOneById($id);

        try {
            $affiliate->setIsActive(false);
            $em->flush();
        } catch(\Exception $e) {
            $this->get('session')->getFlashBag()->add('sonata_flash_error', $e->getMessage());

            return new RedirectResponse($this->admin->generateUrl('list', $this->admin->getFilterParameters()));
        }

        return new RedirectResponse($this->admin->generateUrl('list',$this->admin->getFilterParameters()));
    }
}

Maintenant, créez des templates pour les nouveaux boutons d'action supplémentaires :

  •  src/Erlem/JobeetBundle/Resources/views/AffiliateAdmin/list__action_activate.html.twig
{% if admin.isGranted('EDIT', object) and admin.hasRoute('activate') %}
    <a href={{ admin.generateObjectUrl('activate', object) }} class="btn edit_link" title={{ 'action_activate'|trans({}, 'SonataAdminBundle') }}>
        <i class="icon-edit"></i>
        {{ 'activate'|trans({}, 'SonataAdminBundle') }}
    </a>
{% endif %}
  • src/Erlem/JobeetBundle/Resources/views/AffiliateAdmin/list__action_deactivate.html.twig
{% if admin.isGranted('EDIT', object) and admin.hasRoute('deactivate') %}
    <a href={{ admin.generateObjectUrl('deactivate', object) }} class="btn edit_link" title={{ 'action_deactivate'|trans({}, 'SonataAdminBundle') }}>
        <i class="icon-edit"></i>
        {{ 'deactivate'|trans({}, 'SonataAdminBundle') }}
    </a>
{% endif %}

L'intérieur de votre dossier Admin, ajouter les nouvelles actions et les boutons de la fonction configureListFields, afin qu'ils apparaissent sur la page, pour chaque compte individuellement :

  •  src/Erlem/JobeetBundle/Admin/AffiliateAdmin.php
class AffiliateAdmin extends Admin
{
    // ...    

    protected function configureListFields(ListMapper $listMapper)
    {
        $listMapper
            ->add('is_active')
            ->addIdentifier('email')
            ->add('url')
            ->add('created_at')
            ->add('token')
            ->add('_action', 'actions', array( 'actions' => array('activate' => array('template' => 'ErlemJobeetBundle:AffiliateAdmin:list__action_activate.html.twig'),
                'deactivate' => array('template' => 'ErlemJobeetBundle:AffiliateAdmin:list__action_deactivate.html.twig'))))
        ;
    }
    /// ...
}

Maintenant, effacez le cache:

php app/console cache:clear --env=dev
php app/console cache:clear --env=prod

N'oubliez de créer un utilisateur :

php app/console erlem:jobeet:users admin admin

Connectez-vous : http://jobeet.local/app_dev.php/admin (admin/admin)

04-114-symfony2-jobeet-jour-15-services-web

Voir la liste des affiliés non activés http://jobeet.local/app_dev.php/admin/erlem/jobeet/affiliate/list :

05-114-symfony2-jobeet-jour-15-services-web

C'est tout pour aujourd'hui ! Demain, nous allons apprendre à envoyer des e-mails.


Symfony2 - Jour 14 - Flux de données
[Sommaire] Symfony2 - Jour 16 - Les e-mails >


Pour ce tutoriel, je me suis inspiré du tutoriel Symfony2 Jobeet du site IntelligentBee

Submit to DeliciousSubmit to DiggSubmit to FacebookSubmit to Google PlusSubmit to StumbleuponSubmit to TechnoratiSubmit to TwitterSubmit to LinkedIn