Yoann Aparici

Développement PHP et Symfony2


Il y a de plus en plus de techniques d’optimisation d’affichage des pages Web. Afin de réduire le nombre de requêtes HTTP et ainsi le temps de chargement de votre page, vous pouvez encoder vos données en base 64.

La rfc2397 définit un nouveau schéma d’URL que l’on peut passer aux balises <img />

Voici comment utiliser cette technique dans un projet Symfony2 avec une extension Twig.
Tout d’abord, il va falloir créer notre extension Twig ce qui va nous donner la classe suivante:

namespace Acme\DemoBundle\Twig\Extension;

use Symfony\Component\HttpFoundation\File\File;

class AssetExtension extends \Twig_Extension
{
    public function getFunctions()
    {
        return array(
            'image64' => new \Twig_Function_Method($this, 'image64'),
        );
    }
    
    public function image64($path)
    {
        $file = new File($path, false);
        
        if (!$file->isFile() || 0 !== strpos($file->getMimeType(), 'image/')) {
            return;
        }
        
        $binary = file_get_contents($path);
        
        return sprintf('data:image/%s;base64,%s', $file->guessExtension(), base64_encode($binary));
    }
    
    public function getName()
    {
        return 'demo_asset';
    }
}

Rien de plus simple ! Je crée mon extension Twig et j’y ajoute la méthode qui va me permettre d’effectuer le traitement de mon image, ici image64().
Dans cette méthode je vais utiliser la classe Symfony\Component\HttpFoundation\File\File de Symfony afin de récupérer le type MIME et de m’assurer que je traite bien une image.

Maintenant que mon extension est créée il me faut l’ajouter aux fonctions de mon projet. Pour cela, je vais définir un service avec le tag “twig.extension”

    
<service id="twig.asset_extension" class="Acme\DemoBundle\Twig\Extension\AssetExtension" public="false">
    <tag name="twig.extension" />
</service>

Désormais je pourrai utiliser ma méthode dans mes templates Twig de la façon suivante, en lui donnant bien évidemment le chemin absolu de notre image:

<img src="{{ image64('/path/to/image') }}" />

Bien que réduisant le nombre de requêtes HTTP, cette technique d’encodage d’images ne doit pas être utilisée en permanence. Il est préférable de s’en servir sur des images de petits formats, comme par exemple les avatars des utilisateurs d’un site. Le site google news s’en sert par exemple pour les miniatures qui illustrent les articles.
Pour les plus curieux, vous aurez également remarqué que la barre de développement de Symfony2 utilise également ce procédé.


Il m’est souvent arrivé de devoir faire des exports CSV au cours des différents projets sur lesquels j’ai travaillé. Je vais vous présenter ma façon de faire avec Symfony2.

Pour faciliter les choses, je vais travailler avec une entité qui contient une seule propriété de type tableau, ce qui nous simplifiera grandement le code pour cet exemple.

<?php

namespace Acme\Bundle\ExportBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * Acme\Bundle\ExportBundle\Entity\Answer
* * @ORM\Table(name="answer") */ class Answer { /** * @var integer $id * * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @var array $data * * @ORM\Column(name="data", type="array") */ public $data;
/** * Get id * * @return integer */ public function getId() { return $this->id; }
}


Pour le reste, tout va se passer au niveau de notre controller. La première étape consistera bien évidement à récupérer nos informations. On créera ensuite le fichier CSV et on terminera en renvoyant la réponse, ce qui nous permettra de télécharger notre fichier.
Pour la création du fichier CSV, je choisis de l’écrire directement dans le flux memory de php, ce qui m’évitera de créer physiquement le fichier sur mon serveur.

namespace Acme\Bundle\ExportBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;

class ExportController extends Controller
{
    /**
     * Export answer
     * 
     * @param int $contestId
     * @return Response 
     */
    public function exportAction($contestId)
    {
        $em = $this->getDoctrine()->getEntityManager();
       
        $answers = $em->getRepository('AcmeExportBundle:Answer')->findAll();
        $handle = fopen('php://memory', 'r+');
        $header = array();

        foreach ($answers as $answer) {
            fputcsv($handle, $answer->getData());
        }

        rewind($handle);
        $content = stream_get_contents($handle);
        fclose($handle);
        
        return new Response($content, 200, array(
            'Content-Type' => 'application/force-download',
            'Content-Disposition' => 'attachment; filename="export.csv"'
        ));
    }
}


Mais ce n’est pas tout. En effet, il peut arriver que l’on ait à générer des exports volumineux. Avec cette première méthode, la mémoire va très vite monter et on risque de faire exploser la mémoire de PHP. Pour éviter cela, on va utiliser un itérateur de Doctrine2 qui nous permettra d’éviter cette montée de mémoire.

    public function exportAction($contestId)
    {
        $em = $this->getDoctrine()->getEntityManager();
       
        $iterableResult = $em->getRepository('AcmeExportBundle:Answer')->createQueryBuilder('a')->getQuery()->iterate();
        $handle = fopen('php://memory', 'r+');
        $header = array();

        while (false !== ($row = $iterableResult->next())) {
            fputcsv($handle, $row[0]);
            $em->detach($row[0]);
        }

        rewind($handle);
        $content = stream_get_contents($handle);
        fclose($handle);
        
        return new Response($content, 200, array(
            'Content-Type' => 'application/force-download',
            'Content-Disposition' => 'attachment; filename="export.csv"'
        ));
    }


La mise en place de l’itérateur est très simple et ne change quasiment en rien votre code. En revanche, la différence d’utilisation de mémoire est impressionante puisqu’elle ne bouge quasiment pas pour la deuxième méthode alors qu’elle fait plus que doubler pour 10 000 enregistrements avec la première méthode.

Autre lien :
http://www.doctrine-project.org/blog/doctrine2-batch-processing


J’aurais pu choisir un titre plus accrocheur pour introduire ce post, du genre “Symfony: a voté” ou “Symfony2 ne vote jamais blanc” mais j’ai préféré rester sobre.

Vous l’aurez donc compris je vais vous parler des Voter de Symfony2. Ils font parti du composant Security et vont nous permettre de déterminer des droits d’accès.
Pour définir un voter, il suffit de créer une classe qui va implémenter VoterInterface et ensuite la déclarer en temps que service.

L’exemple que je vais donner dans le cadre de cet article n’est là que pour illustrer l’utilisation des Voter. Il est possible que ce ne soit pas le plus pertinent mais ce n’est pas le but de cet article.

<?php

namespace Acme\Bundle\Security\Authorization\Voter;

use Acme\Bundle\Entity\Foobar;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;

class OwnerVoter implements VoterInterface
{
    public function supportsAttribute($attribute)
    {
        return 'OWNER' === $attribute;
    }

    public function supportsClass($class)
    {
        return $class instanceof Foobar;
    }

    function vote(TokenInterface $token, $object, array $attributes)
    {
        foreach ($attributes as $attribute) {
            if ($this->supportsAttribute($attribute) && $this->supportsClass($object)) {
                $user = $token->getUser();

                if ($object instanceof Foobar && $user->equals($object->getUser())) {
                    return VoterInterface::ACCESS_GRANTED;
                }
            }
        }
        
        return VoterInterface::ACCESS_DENIED;
    }
}

Je vais ensuite déclarer cette classe comme un service : l’important ici est de déclarer le tag “security.voter”. Cela va permettre d’ajouter votre classe au “Manager d’accès”.

<service id="security.access.owner" class="Acme\Bundle\Security\Authorization\Voter\OwnerVoter">
<tag name="security.voter" /> </service>

Quelques explications s’imposent. Tout d’abord, notre “Voter” doit contenir 3 méthodes qui vont lui permettre de fonctionner normalement.
La première va nous permettre de définir quels attributs sont supportés. En d’autres termes, quels droits doit avoir notre utilisateur pour que l’accès lui soit autorisé.
La seconde méthode va nous permettre de définir sur quel type d’objet (ou interface) notre “Voter” s’applique.
La dernière méthode va nous permettre de donner ou refuser l’accès.

Ici nous voyons donc que nous vérifions que l’attribut soit “OWNER” et que la classe pour laquelle nous voulons restreindre l’accès est “Foobar” et que notre utilisateur connecté soit le même que l’utilisateur à qui appartient l’objet.

Désormais, je vais pouvoir facilement restreindre l’accès à des Controller ou ne pas afficher certaines données dans une vue.

public function deleteAction($id)
{
    $em = $this->getDoctrine()->getEntityManager();
    $foobar = $em->getRepository('AcmeBundle:Foobar')->find($id)

    if (!$this->get('security.context')->isGranted('OWNER', $foobar)) {
        throw new AccessDeniedException();
    }
}
{% if is_granted('OWNER', foobar) %}
Hello World!
{% endif %}

Les “Voter” vous permettent donc de définir les droits d’accès. Ici l’exemple est primaire mais grâce à la DIC, on peut très vite imaginer d’autres utilisations comme par exemple créer des rôles dynamiques.

D’autres articles:

http://kriswallsmith.net/post/15994931191/symfony2-security-voters
http://symfony.com/doc/2.0/cookbook/security/voters.html