Criando extensão do Twig que renderiza html, com e sem template

Salve,

Estava precisando criar uma extensão do Twig, para rodar no Symfony2, que me retornasse o html e o mesmo, não fosse mostrado em código html, mas sim renderizado. Pesquisando achei a solução e é bem fácil.

Passe no array de parâmetros $options  a chave is_safe com valor ['html']  no construtor da Twig_Function_Method:

<?php
// src/Base/BaseBundle/Twig/MyTitleExtension.php
namespace Base\BaseBundle\Twig;

use Symfony\Component\HttpKernel\KernelInterface;

class MyTitleExtension extends \Twig_Extension
{
    private $kernel;

    public function __construct(KernelInterface $kernel)
    {
        $this->kernel = $kernel;
    }

    public function getFunctions()
    {
        return [
            'myTitle' => new \Twig_Function_Method($this, 'myTitle', ['is_safe' => ['html']]),
        ];
    }

    public function myTitle($title)
    {
        return '<h1>' . $title . '</h1>';
    }

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

Desse jeito, basta utilizar {{ myTitle('Meu título aqui!') }} e renderizará o código <h1>Meu título aqui!</h1>. Sem isso, seria escapado o html utilizando htmlentities() e seria impresso o código html ao invés de renderizá-lo.

Agora, você poderá querer separar os papéis e jogar o html num template, assim, além de todos os benefícios de organização, seguir boas práticas, vocês poderá customizar a qualquer momento sem nem tocar no código da extensão. Vamos lá!

Faremos 2 mudanças em relação ao código anterior.

A primeira é passando para a Twig_Function_Method via $options que essa função precisa o ambiente do Twig, que é uma instância da classe Twig_Enviroment, através da chave needs_environment com o valor true:

Para não repetir muito código, irei passa somente o método modificado.

<?php
// src/Base/BaseBundle/Twig/MyTitleExtension.php

// código anterior

    public function getFunctions()
    {
        return [
            'myTitle' => new \Twig_Function_Method(
                $this, 
                'myTitle', 
                [
                    'is_safe' => ['html'],
                    'needs_environment' => true,
                ]
            ),
        ];
    }

// código posterior

A segunda é adicionar ao método myTitle, como primeiro parâmetro sempre, o parâmetro $twig. E daí você poderá chamar o render e passar demais parâmetros e etc para a view:

<?php
// src/Base/BaseBundle/Twig/MyTitleExtension.php

// código anterior

    public function myTitle(\Twig_Environment $twig, $title)
    {
        return $twig->render(
            'BaseBaseBundle::myTitle.html.twig',
            [
                'title' => $title,
            ]
        );
    }

// código posterior

A view:

{# src/Base/BaseBundle/Resources/views/myTitle.html.twig #}
<h1 class="big-title">{{ title }}</h1>

Com isso, quando você precisar mudar algo em relação ao html do título, basta mudar a view, e ficar longe do código da extensão, o que diminui a probabilidade de criar bug. E principalmente, fica mais organizado e padronizado.

Espero que ajude!

Forçando HTTPS no Symfony2

Salve,

Em muitos casos, possuímos a necessidade de que as URL’s (rotas) sejam acessadas via HTTPS (SSL) (todo mundo tem medo da NSA :D), sejam todas ou algumas específicas. Então ensinarei como fazer.

Caso você deseje forçar o HTTPS somente em alguma rota específica, tipo login ou cadastro, a maneira mais rápida é fazer via app/config/security.yml:

access_control:
        - { path: ^/login$, role: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel: https }
        - { path: ^/register, role: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel: https }

Mas em casos onde você precisa rodar todo o sistema sobre HTTPS, dessa maneira ai fica bem ruim, além de cansativo. Então vamos mudar no app/config/routing.yml:

acme_bar:
    resource: "@AcmeBarBundle/Controller"
    prefix:   /
    type:     annotation
    schemes: [https]

Isso diz que todas as rotas desse controller, só serão acessadas via HTTPS, até mesmo usando o path(), ele vai gerar com HTTPS. Lembrando que se você utilizar outro tipo de configuração de rota, a configuração é a mesma.

Então é isso.

Documentação: http://symfony.com/doc/current/cookbook/routing/scheme.html

Intregrando Plupload com Symfony2

Salve,

Necessitando utilizar o Plupload para envio de arquivos em massa em um projeto Symfony2, achei um bundle que faz toda a integração de backend, com suporte a coisas interessantes e de forma simples e fácil de utilizar. Esse bundle é o OneupUploaderBundle.

Para instalar é fácil:

composer require oneup/uploader-bundle

Quando pedir a versão, informe: ~1.3. Ou adicione no composer.json:

"oneup/uploader-bundle": "~1.3"

Após instalar o pacote, registre o bundle no app/AppKernel.php:

new Oneup\UploaderBundle\OneupUploaderBundle(),

Agora configure ele em app/config/config.yml:

oneup_uploader:
    mappings:
        fotos:
            frontend: plupload

Configure a rota automática em app/config/routing.yml:

oneup_uploader:
    resource: .
    type: uploader

Na view que deseja, configure:

<!-- CSS -->
<link href="{{ asset('bundles/meuapp/css/jquery-ui-1.10.3.custom.min.css') }}" rel="stylesheet" media="screen"/>
<link href="{{ asset('bundles/meuapp/js/plupload-2.1.2/js/jquery.ui.plupload/css/jquery.ui.plupload.css') }}" rel="stylesheet"/>
<!-- Javascript -->
<script type="text/javascript" src="http://bp.yahooapis.com/2.4.21/browserplus-min.js"></script>
<script src="{{ asset('bundles/meuapp/js/jquery-ui-1.10.3.custom.min.js') }}"></script>
<script src="{{ asset('bundles/meuapp/js/plupload-2.1.2/js/plupload.full.min.js') }}"></script>
<script src="{{ asset('bundles/meuapp/js/plupload-2.1.2/js/jquery.ui.plupload/jquery.ui.plupload.min.js') }}"></script>
<script src="{{ asset('bundles/meuapp/js/plupload-2.1.2/js/i18n/pt_BR.js') }}"></script>

<div id="fileupload"></div>

<script type="text/javascript">
    $(document).ready(function() {
        $("#fileupload").plupload({
            runtimes: "html5,flash,silverlight",
            url: "{{ oneup_uploader_endpoint('fotos') }}", // fotos é o nome do mapeamento, que foi definido no config.yml
            // Flash settings
            flash_swf_url: '{{ asset('bundles/meuapp/js/plupload-2.1.2/js/Moxie.swf') }}',
            // Silverlight settings
            silverlight_xap_url: '{{ asset('bundles/meuapp/js/plupload-2.1.2/js/Moxie.xap') }}'
        });
    });
</script>

Praticamente está funcionando. Ele vai salvar em web/uploads/fotos (fotos é o nome do mapeamento no config.yml). Se quiser realizar tarefas após o upload com cada arquivo, só fazer um listener, onde se recebe dados via parâmetro $event, onde tem acesso ao arquivo via $event->getFile().

Para melhorar e fazer mais coisas, a documentação é boa, com exemplos e tem funcionalidades bacanas: https://github.com/1up-lab/OneupUploaderBundle/blob/master/Resources/doc/index.md

Listener:

Valeu!

 

 

 

Importação de dados em massa, PHP, Doctrine e problemas

Salve,

Estava numa necessidade de importar uma quantidade grande de dados, para algumas pessoas não tão grande, que estava em um CSV. Os dados eram as cidades de todo o planeta e a quantidade de registros é imensa, quase 3milhões de linhas.

O sistema está usando Symfony2 com Doctrine2 e fiz uns comandos para importar países, estados e faltava cidades. Os outros foram beleza, teve certa demora mas não passou de 4mil linhas, com no mínimo 2 consultas ao banco antes de gravar.

Para importar cidades, segui o mesmo padrão: carregar a linha, consulta o estado a partir do código ISO do país + o código da região, se existir, checo se a cidade existe, se não existir, gravava. Mas não deu certo. Fiz ajustes no código para dá insert a cada 200 itens na fila de persistência, depois aumentei e o máximo que consegui foi 200mil registros. Muito, se comparar aos que já tinha feito, mas pouco se comparar ao total que eu tinha que conseguir. Fiz otimizações até chamando função do coletor de lixo do PHP, ajudou, mas não suficiente. Sem elas não passava de 150mil registros.

Até que eu lembrei do Redis, suas diversas estruturas de dados, sua capacidade de salvar cópia em disco e mais outras coisas, sou apaixonado pelo Redis :D.

Mudei a estratégia para fazer funcionar. Pelo fato de que talvez nunca houvesse uma atualização de cidades após essa importação, e tratando para excluir as cidades brasileiras, já que elas já existiam no banco de dados e essa importação tem o objetivo de fazer a aplicação suportar cidades do mundo todo nos seus cadastros. Passei a não checar se o estado e a cidade existiam, descartei o insert na hora, pois pior que trabalhar com milhões de registros num arquivo texto é trabalhar com milhões de consultas ou operações de gravação no banco de dados, é um caos.

Para a solução comecei a por os estados (~4mil) em uma estrutura de Hash, no Redis, com a chave sendo o código iso (ISO do país + código da região) e o valor sendo o ID do estado no banco de dados, usando uma consulta somente, a única envolvida nessa mudança.

Com esses dados no Redis, varri as cidades e gerando um código SQL de insert e guardando numa outra estrutura do Redis, a List, esse código. Com isso em menos de 25 minutos já tinha todas as cidades no Redis, com o código de insert correto, dando um total de 2.884.445 de cidades. Mas ainda tinha um problema, ainda não estava no banco de dados da aplicação, que é um MySQL.

Após ter todos os dados gerados corretamente, bastam 2 comandos: um para exportar os inserts para um arquivo .sql e outro pra importar o .sql no MySQL.

$ redis-cli LRANGE cities 0 -1 > insert.cities.sql
$ mysql -u root meu_banco < insert.cities.sql

Creio que seja uma ótima solução e que não faz sair totalmente do ambiente PHP e usar outras linguagens, apesar de ainda ter que exportar e importar o arquivo sql, para processamento total. Além de ser bem rápido e simples.

Após perder várias horas tentando fazer um milagre sem sair do ambiente que estava, estou bastante satisfeito com a solução como um todo. E mais apaixonado pelo Redis :D

 

Symfony2: campo do tipo Entity, no Form type, com o texto do item usando diversas propriedades da entidade

Salve,

Estava com uma necessidade de aparecer num <select> a informação de duas propriedades da entidade utilizada, e achei, não de maneira fácil mas que parece ser uma saída elegante. Supondo que nossa entidade tem dois campos, codigo e nome, e no <option> preciso mostrar essas duas propriedades seguindo o padrão de formatação: $codigo$nome

<Form\MeuFormType.php>

->add(
    'meuTipo',
    'entity',
    array(
        'label' => 'Tipo',
        'class' => 'AcmeBaseBundle:MeuTipo',
        'property' => 'label',
    )
)

<Entity\MeuTipo.php>

/**
 * @return string
 */
public function getLabel()
{
    return $this->getCodigo() . ' - ' . $this->getNome();
}

 

Explicando:

Basta criar um método na entidade que retorne uma string, não precisa ter a propriedade do método, pois apesar da configuração se chamar property, ele vai tentar acessar via métodos de acesso get(), has(), is*() e __get().

Fica ai a dica. Espero ter ajudado.

Filtro em coleção no Doctrine

Salve,

As vezes temos uma situação onde precisamos filtrar dados retornado de um relacionamento entre entidades, onde o fruto desse relacionamento é uma coleção de uma das 2 entidades, mais especificamente numa relação One to Many. O Doctrine retorna um ArrayCollection, que é uma estrutura do Doctrine que implementa vários tipos de estruturas do PHP como: Countable, IteratorAggregate e ArrayAccess.

Com isso você pode fazer diversas operações sobre a coleção, mas uma das mais bacanas que faz poupar consultas extras ao banco somente para retornar os dados filtrados como deseja é a matching(Criteria $criteria), que receber uma instância de Doctrine\Common\Collections\Criteria com as suas devidas regras de filtragem.

Um uso que faço é quando tenho nessa coleção registros com um determinado campo que desejo usar como filtro, um exemplo é uma entidade de mídia, onde se tem um campo com o tipo da mídia (vídeo, imagem, slide e etc), e você deseja filtrar essa coleção, que é acessível facilmente pelas mágicas do Doctrine, sem ter que fazer uma consulta manualmente específica para isso, trazendo as mídias que tem um determinado valor naquele ou naqueles campos:

/**
 * @ORM/Entity()
 */
class Album
{
    // propriedades

    /**
     * @ORM\OneToMany(targetEntity="Acme\MediaBundle\Entity\Media", mappedBy="album")
     */
    private $files;

    // getters e setters
}

/**
 * @ORM/Entity()
 */
class Media
{
    // propriedades

    /**
     * @var \Acme\MediaBundle\Entity\Album
     *
     * @ORM\ManyToOne(targetEntity="Acme\MediaBundle\Entity\Album", inversedBy="files")
     * @ORM\JoinColumn(name="album_id", referencedColumnName="id", nullable=true)
     */
    protected  $album;

    // getters e setters
}

Quando você fizer um $album->getFiles(), irá retornar todas as mídias de todos os tipos. Ai usamos o Criteria no ArrayCollection sobre o matching() para filtrar, adicionando um método simples na entidade Album, por exemplo:

Somente imagens

    public function getImagesFiles() {
        $criteria = Criteria::create()
            ->where(Criteria::expr()->eq('type', Media::TYPE_IMAGE));

        return $this->getFiles()->matching($criteria);
    }

 Somente vídeos:

    public function getVideosFiles() {
        $criteria = Criteria::create()
            ->where(Criteria::expr()->eq('type', Media::TYPE_VIDEO));

        return $this->getFiles()->matching($criteria);
    }

Ficando assim a entidade com as mudanças:

use Doctrine\Common\Collections\Criteria;

/**
 * @ORM/Entity()
 */
class Album
{
    // propriedades

    /**
     * @ORM\OneToMany(targetEntity="Acme\MediaBundle\Entity\Media", mappedBy="album")
     */
    private $files;

    // getters e setters

    // Images
    public function getImagesFiles() {
        $criteria = Criteria::create()
            ->where(Criteria::expr()->eq('type', Media::TYPE_IMAGE));

        return $this->getFiles()->matching($criteria);
    }

    // Videos 
    public function getVideosFiles() {
        $criteria = Criteria::create()
            ->where(Criteria::expr()->eq('type', Media::TYPE_VIDEO));

        return $this->getFiles()->matching($criteria);
    }
}

Você pode usar o Criteria para criar diversos filtros.

Então fica ai uma ajuda para tentar evitar ficar fazendo diversas consultas e códigos extras e confusos para filtros uma coleção.

Valeu!

Doctrine 2: consulta com DQL envolvendo tabela many to many nos joins

Salve,

Passando por problemas em como fazer uma consulta onde em um determinado JOIN teria uma tabela de relacionamento many to many, tive muitos problemas, principalmente na documentação, pois não explica algumas coisas.

Na documentação descobrir o MEMBER OF, que buscar o valor em uma coleção. Lá dá um único exemplo e uso o mesmo na cláusula WHERE:

<?php
$query = $em->createQuery('SELECT u.id FROM CmsUser u WHERE :groupId MEMBER OF u.groups');
$query->setParameter('groupId', $group);
$ids = $query->getResult();

http://docs.doctrine-project.org/en/2.1/reference/dql-doctrine-query-language.html

Desse jeito, funciona, somente no WHERE. Mas meu caso era para pegar registros de um usuário, mas o único relacionamento de entidades entre esses registros que eu necessitava e o usuário (só tinha o ID dele), era uma entidade com many to many, entre os relacionamentos many to one nas extremidades, seguindo essa idéia:

Usuário -> m2o -> Pagamento -> m2o -> Plano -> m2m -> Produto

* m2o = Many To One
** m2m = Many To Many

Após apanhar com erros consegui achar a solução, que é até aceitável quando você para pra pensar: O MEMBER OF, quando está no JOIN ele não recebe um valor simples (como um ID) mas sim um objeto, para checar se existe dentro da coleção:

$dql = "SELECT p
        FROM MyAdminBundle:Payment p
            INNER JOIN p.email e
            INNER JOIN e.user u
            INNER JOIN p.plan pl
            INNER JOIN MyAdminBundle:Product pr WITH pr MEMBER OF pl.products
        WHERE u.id = :userId";

O entendimento dela é como o contains do ArrayCollection do Doctrine:

$plan->getProducts()->contains($product);//$product é um objeto da entidade Product

Após apanhar bastante, conseguir resolver esse problema que estava me matando por não está na documentação desse modo.

Valeu!

Symfony2: injetando o service container em serviços

Salve,

As vezes precisamos acessar outros serviços dentro de um determinado serviço e o Symfony possui o Service Container para retornar instâncias dos serviços com as suas dependências sendo injetadas. Quando registramos esses serviços, passamos eles como parâmetros, um a um. Mas existem casos em que podemos deixar de lado isso e trabalhar internamente com o Service Container para poder acessar qualquer serviço registrado na aplicação.

O método de registro é igual em ambos os casos:

acme_testbundle.services.meu_servico:
class: Acme\TestBundle\Service\MeuServico

arguments:
    - "@doctrine.orm.entity_manager"
    - "@mailer"
    - "@templating"

Fica:

acme_testbundle.services.meu_servico:
        class: Acme\TestBundle\Service\MeuServico
        arguments: ["@service_container"]

Muda agora na classe, assim:

<?php

namespace Acme\TestBundle\Service;

use Symfony\Component\DependencyInjection\ContainerInterface;

class MeuServico
{
    protected $container;

    public function __construct(ContainerInterface $container)
    {
        $this->container = $container;
    }
}

Agora você pode utilizar na classe o Service Container para recuperar seus serviços, assim:

$this->container->get(‘acme_testbundle.services.meu_servico');

Valeu!

Symfony2: testes funcionais com autenticação via WSSE

Salve,

Após apanhar bastante pesquisando em como fazer funcionar os testes funcionais de recursos que estão usando uma autenticação WSSE, no próprio Symfony2, um conhecido me deu a solução de como passar os cabeçalhos:

Cenário

Você já tem seu teste ok, passando a url e os parâmetros e está funcionando se você suspender o uso do WSSE no recurso, mas com ele ativo, ele não passa da autenticação do WSSE, logo os testes não rodam.

Para fazer os testes, está utilizando o client http do provido pelo próprio framework/WebTestCase. Exemplo de criação do client http:

$this->client = static::createClient(…);

E no request está assim:

$crawler = $this->client->request(
‘GET’,
‘/meu/recurso’
);

A solução

O quinto parâmetro do request é o de variáveis $_SERVER, que é onde você terá que colocar os cabeçalhos. O problema é que por padrão o cabeçalho http precisa possuir o prefixo HTTP_ para ele ser considerado/visto no client http.

Então, se você possui dos cabeçalhos Authorization, User-Agent e X-WSSE, você simplesmente transforma eles para:  HTTP_AUTHORIZATION, HTTP_USER_AGENT e HTTP_X-WSSE.

E passa isso num array no quinto parâmetro do request:

$crawler = $this->client->request(
‘GET’,
‘/meu/recurso’,
array(),
array(),
array(‘HTTP_AUTHORIZATION’ => ‘Authorization profile=”XXX”‘,
‘HTTP_USER_AGENT’ => ‘XXX’,
‘HTTP_X-WSSE’ => ‘XXX’)

);

Onde tem XXX você irá trocar pelo valor necessário, principalmente no HTTP_X-WSSE que possui regras para geração dos dados.

Valeu!