Symfony Mailer – wysyłanie maili w PHP

Wraz z Symfony 4.3 pojawił się nowy komponent do wysyłania maili – Mailer oraz uzupełniający go moduł Mime służący do tworzenia wiadomości. W założeniu te dwie nowości mają zastąpić Swift Mailer, który jest biblioteką mającą ponad kilkanaście lat i część rozwiązań z niego jest przestarzałych. Te dwa komponenty dają ogromne możliwości:

  • tworzenie wieloczęściowych wiadomości (multipart),
  • integracja z Twigiem,
  • inlinowanie CSSów,
  • dodawanie załączników i wiele innych.

Instalacja

Instalujemy komponent po przez uruchomienie następującej komendy:


composer require symfony/mailer

Konfiguracja transportu

Emaile są dostarczane poprzez transporty. Domyślnie możemy wysyłać emaile przez SMTP. Ustawiamy adres serwera w pliku .env


# .env
MAILER_DSN=smtp://user:pass@smtp.example.com

Możemy dodać inne serwisy poprzez dodanie transportów do composera.

TransportKomenda
Amazon SEScomposer require symfony/amazon-mailer
Gmailcomposer require symfony/google-mailer
MailChimpcomposer require symfony/mailchimp-mailer
Mailguncomposer require symfony/mailgun-mailer
Postmarkcomposer require symfony/postmark-mailer
SendGridcomposer require symfony/sendgrid-mailer

Jeśli przykładowo chcemy użyć Gmaila odpalamy:


composer require symfony/google-mailer

Każdy z serwisów ma własną receptę dla Flexa, także potrzebne ustawienia będą dostępne w plikach konfiguracyjnych. Dla Gmaila dostajemy w pliku .env


# .env
###> symfony/google-mailer ###
# Gmail SHOULD NOT be used on production, use it in development only.
GMAIL_USERNAME=email@gmail.com
GMAIL_PASSWORD=foobar
MAILER_DSN=smtp://$GMAIL_USERNAME:$GMAIL_PASSWORD@gmail
###< symfony/google-mailer ###

MAILER_DSN to nie jest prawdziwy adres SMTP, tylko konwencja konfiguracji. Zmienne GMAIL_USERNAME oraz GMAIL_PASSWORD możemy ustawić w pliku .env lub .env.local

Tworzenie wiadomości

Aby wysłać email potrzebujemy wstrzyknąć MailerInterface oraz utworzyć obiekt Email.


namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;
use Symfony\Component\Mime\NamedAddress;
use Symfony\Component\Routing\Annotation\Route;

class MailerController extends AbstractController
{
    /**
     * @Route("/mailer")
     */

    public function index(MailerInterface $mailer)
    {
        $email = (new Email())
            ->to('piotr@example.com')
            //->cc('user@example.com')
            //->bcc('bcc@example.com')
            //->replyTo('piotr@example.com')
            //->priority(Email::PRIORITY_HIGH)
            ->subject('Test Symfony Mailera')
            ->text('Zwykły tekst')
            ->html('<p>Email HTML</p>')
        ;

        $mailer->send($email);
       
        // ...
    }

Adres email

Wszystkie metody, które wymagają adresu email (->from, ->to, ->cc, ->bcc) , przyjmują jako parametr string albo obiekty adresu.


use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\NamedAddress;

$email = (new Email())
    // email jako string
    ->to('piotr@example.com')

    // email address jako obiekt
    ->to(new Address('piotr@example.com'))

    // email address jako obiekt (widoczna będzie nazwa zamiast adresu)
    ->to(new NamedAddress('piotr@example.com', 'Piotr'))
;

Możemy dodać wiele adresów jako (to, cc, bcc)


$email = (new Email())
    ->bcc('test@example.com')
    ->addBcc('test2@example.com')
    ->addBcc('test3@example.com')
;

Ewentualnie jako parametry


$email = (new Email())
    ->cc('test2@example.com', 'test4@example.com')
    ->bcc(['test@example.com', 'test2@example.com'])
;

Treść wiadomości

Możemy dołączyć treść tekstową i HTML zarówno jako stringi, jak i resource (np. fopen, streams).


$email = (new Email())
    ->text('Test...')
    ->html('<p>Test...</p>')

    // strumienie
    ->text(fopen('/emaile/rejestracja.txt', 'r'))
    ->html(fopen('/emaile/rejestracja.html', 'r'))
;

Załączniki

Załączniki możemy dodawać na dwa sposoby. Po pierwsze przy użyciu attachFromPath i podaniu ścieżki


$email = (new Email())
    ->attachFromPath('/zalaczniki/regulamin.pdf')
    // opcjonalnie można podać inną nazwę dla pliku
    ->attachFromPath('/zalaczniki/regulamin-v3.pdf', 'Regulamin.pdf')
    // można również podać typ MIME, który nie jest wówczas zgadywany
    ->attachFromPath('/zalaczniki/regulamin-v3.pdf', 'Regulamin.pdf', 'application/pdf')
    // można podać absolutny url
    ->attachFromPath('http://example.com/zalaczniki/regulamin.pdf', 'Regulamin.pdf', 'application/pdf')
;

Można skorzystać również z metody attach i podać strumień:


$email = new Email();
$email->attach(fopen('/sciezka/pliku.pdf', 'r'));

Zagnieżdżanie obrazków

Kiedy chcesz wyświetlić obrazek w emailu, powinieneś najpierw go zagnieździć a później użyć w treści emaila. Kiedy używasz Twiga, nie musisz tego robić, Twig sam zagnieżdża obrazki.


$email = new Email();
$email->embed(fopen('/sciezka/do/obrazka.png', 'r'), 'logo');
$email->embedFromPath('/sciezka/do/podpis.png', 'podpis');

$email->html('<img src="cid:logo"> ... <img src="cid:podpis">');

Tak jak widzimy żeby użyć obrazka w treści emaila należy użyć skład “cid:” + “nazwa obrazka”, którą wcześniej zdefiniowaliśmy.

Globalne działanie przy wysyłce

Przy każdej wysyłce wiadomości jest tworzony event do którego możemy się podpiąć. Na przykład jeśli chcemy wysyłać do siebie kopie każdego maila, możemy dodać ukrytego odbiorce bcc:


<?php

namespace App\EventListener;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Mailer\Event\MessageEvent;
use Symfony\Component\Mime\Email;
use Symfony\Component\Mime\NamedAddress;

class MailerBccCopyListener implements EventSubscriberInterface
{
    public function onMessageSend(MessageEvent $event)
    {
        $message = $event->getMessage();

        if (!$message instanceof Email) {
            return;
        }

        $message->bcc(new NamedAddress('piotr@example.com', 'Kopia maila'));
    }
   
    /**
     * {@inheritDoc}
     */

    public static function getSubscribedEvents()
    {
        return [MessageEvent::class => 'onMessageSend'];
    }
}

Twig

Dzięki integracji z Twigiem, możemy w łatwy sposób utrzymać porządek w szablonach, zagnieżdżać style, używać frameworków CSS. Żeby użyć Twiga, dodajmy go do composera.


composer require symfony/twig-bundle

Treść HTML

Żeby stworzyć email przy użyciu Twig, użyjemy klasy TemplatedEmail, która dziedziczy po klasie Email


$email = (new TemplatedEmail())
    ->to(new NamedAddress('piotr@example.com', 'Piotr Belina'))
    ->subject('Test Symfony Mailera')

    // tutaj podajemy ścieżkę do szablonu Twig
    ->htmlTemplate('emails/signup.html.twig')

    // zmienne kontekstowe dla szablonu
    ->context([
        'user' => 'Ania',
        'expiration_date' => new \DateTime('+7 days'),
    ])
;

Szablon dodajemy do templates/emails/signup.html.twig


<h1>Witaj {{ email.toName }}!</h1>

<p>
    Zarejestrowałeś się jako {{ user }} używając poniższego emailu:
</p>
<p><code>{{ email.to[0].address }}</code></p>

<p>
    <a href="#">Kliknij aby aktywować konto</a>
    (ten link jest ważny do {{ expiration_date|date('F jS') }})
</p>

W szablonie mamy dostęp do wszystkich zmiennych, które przekazaliśmy w kontekście oraz do specjalnego obiektu email, który jest instancją klasy WrappedTemplatedEmail.

Treść tekstowa

Jeśli nie dodamy treści tekstowej dla emaila, zostanie ona automatycznie wygenerowana. Można dodać własną treść w ten sposób.


$email = (new TemplatedEmail())
    //...

    // tutaj podajemy ścieżkę do szablonu Twig
    ->htmlTemplate('emails/signup.html.twig')
    ->textTemplate('emails/signup.txt.twig')

    // ...
;

Symfony 4 – jak stworzyć nowy projekt

Symfony to obecnie jeden z najbardziej rozwiniętych frameworków w świecie PHP. Może wydawać się z początku skomplikowany w użyciu, jednak to tylko pozory. W rzeczywistości aplikację internetowe z zużyciem Symfony pisze się bardzo łatwo. Nie musimy się martwić o wiele podstawowych elementów, skupiamy się na tym co jest istotne – na funkcjonalności naszej strony internetowej.

Jak zainstalować Symfony

Zanim zainstalujemy Symfony 4, musimy mieć PHP w wersji conajmniej 7.1 oraz Composera (instalację Composera opisałem tutaj).

Aby stworzyć nowy projekt wpisujemy następujące polecenie w linii komend


composer create-project symfony/website-skeleton nowy-projekt

Jak widzimy, Composer potrafi również tworzyć projekty na podstawie szkieletu. Composer ściągnął potrzebne pakiety oraz stworzył strukturę naszego projektu. (W tym przykładzie używamy symfony/website-skeleton, który jest przeznaczony dla tradycyjnych aplikacji internetowych. Dostępny jest również symfony/skeleton, który jest okrojoną wersją, świetnie nadający się na mikroserwisy, aplikacje konsolowe, API itp.)

Struktura katalogów po instalacji wygląda tak

Po instalacji powinniśmy zobaczyć następujący widok na ekranie konsoli z podpowiedziami co możemy zrobić dalej.


 What's next?


  * Run your application:
    1. Change to the project directory
    2. Create your code repository with the git init command
    3. Run composer require server --dev to install the development web server,
       or configure another supported web server https://symfony.com/doc/current/setup/web_server_configuration.html

  * Read the documentation at https://symfony.com/doc


 Database Configuration


  * Modify your DATABASE_URL config in .env

  * Configure the driver (mysql) and
    server_version (5.7) in config/packages/doctrine.yaml


 How to test?


  * Write test cases in the tests/ folder
  * Run php bin/phpunit

Uruchamianie projektu

W celu uruchomienia projektu w przeglądarce możemy albo skonfigurować nasz serwer www (Apache, nginx itp.) tak aby odczytywał pliki z katalogu public. Lub prościej możemy użyć wbudowanego serwera w PHP. W tym celu przejdźmy do katalogu z projektem i uruchomimy serwer:

cd nowy-projekt
php bin/console server:run

Naszym oczom powinien ukazać się taki widok:

W tym momencie możemy przejść pod adres http://127.0.0.1:8000 w przeglądarce i powinniśmy zobaczyć ekran powitalny Symfony:

Gratulacje! Właśnie stworzyłeś i uruchomiłeś swój projekt Symfony 4.

Git – dodanie projektu do systemu kontroli wersji

Ostatnim krokiem jaki powinniśmy zrobić to utworzenie repozytorium z naszym projektem i dodanie plików do pierwszego commita.

W tym celu wpisujemy w wierszu poleceń


git init
git add .
git commit -m "pierwszy commit"

W projekcie jest domyślnie utworzony plik .gitignore także nie musimy się martwić o wykluczenie np. katalogu vendor, var/cache czy var/logs.

Instalacja Composera

Composer to narzędzie do zarządzania zależnościami w projekcie. Możesz dzięki niemu w łatwy sposób dołączać biblioteki lub uaktualniać je.

Instalacja w linii komend

Skopiuj i uruchom w linii komend następujący kod.


php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
php -r "if (hash_file('sha384', 'composer-setup.php') === '48e3236262b34d30969dca3c37281b3b4bbe3221bda826ac6a9a62d6444cdb0dcd0615698a5cbe587c3f0fe57a54d8f5') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
php composer-setup.php
php -r "unlink('composer-setup.php');"

Instalacja w Windowsie przez instalator

Możesz ściągnąć instalator bezpośrednio ze strony Composera. Po uruchomieniu instalator zainstaluje Composer i uaktualni PATH sprawiając, że Composer będzie dostępny wszędzie w linii komend.

Vagrant, Chef, PHP and XDebug

How to install XDebug with Vagrant and Chef

  1. Install Chef Development Kit
  2. Create new cookbook
    ➜ dev berks cookbook xdebug-box
    create xdebug-box/files/default
    create xdebug-box/templates/default
    create xdebug-box/attributes
    create xdebug-box/libraries
    create xdebug-box/providers
    create xdebug-box/recipes
    create xdebug-box/resources
    create xdebug-box/recipes/default.rb
    create xdebug-box/metadata.rb
    create xdebug-box/LICENSE
    create xdebug-box/README.md
    create xdebug-box/CHANGELOG.md
    create xdebug-box/Berksfile
    create xdebug-box/Thorfile
    create xdebug-box/chefignore
    create xdebug-box/.gitignore
    create xdebug-box/Gemfile
    create .kitchen.yml
    conflict chefignore
    Overwrite /Users/bela/dev/xdebug-box/chefignore? (enter "h" for help) [Ynaqdh]
    force chefignore
    append Thorfile
    create test/integration/default
    append .gitignore
    append .gitignore
    append Gemfile
    append Gemfile
    You must run `bundle install' to fetch any new gems.
    create xdebug-box/Vagrantfile
  3. Go to directory
    cd xdebug-box
  4. Install bundles
    ➜  xdebug-box git:(master) ✗ bundle install
    Fetching gem metadata from https://rubygems.org/........
    Fetching additional metadata from https://rubygems.org/..
    Resolving dependencies...
    Using addressable 2.3.8
    Using multipart-post 2.0.0
    Using faraday 0.9.1
    Using httpclient 2.6.0.1
    Using berkshelf-api-client 1.3.0
    Using buff-extensions 1.0.0
    Using hashie 2.1.2
    Using varia_model 0.4.0
    Using buff-config 1.0.1
    Using buff-ruby_engine 0.1.0
    Using buff-shell_out 0.2.0
    Using hitimes 1.2.2
    Using timers 4.0.1
    Using celluloid 0.16.0
    Using nio4r 1.1.1
    Using celluloid-io 0.16.2
    Using cleanroom 1.0.0
    Using minitar 0.5.4
    Using sawyer 0.6.0
    Using octokit 3.8.0
    Using retryable 2.0.2
    Using buff-ignore 1.1.1
    Using erubis 2.7.0
    Using json 1.8.3
    Using mixlib-log 1.6.0
    Using mixlib-authentication 1.3.0
    Using net-http-persistent 2.9.4
    Using semverse 1.2.1
    Using ridley 4.2.0
    Using dep-selector-libgecode 1.0.2
    Using ffi 1.9.10
    Using dep_selector 1.0.3
    Using solve 1.2.1
    Using thor 0.19.1
    Using berkshelf 3.3.0
    Using mixlib-shellout 2.1.0
    Using net-ssh 2.9.2
    Using net-scp 1.2.1
    Using safe_yaml 1.0.4
    Using test-kitchen 1.4.2
    Using kitchen-vagrant 0.18.0
    Using bundler 1.7.5
    Your bundle is complete!
    Use `bundle show [gemname]` to see where a bundled gem is installed.
  5. Add dependencies to your metadata.rb
    depends 'apache2', '~>; 3.1.0'
    depends 'php', '~> 1.7.2'
    depends 'xdebug', '~> 1.0.0'
  6. Comment out 3 lines in Vagrantfile
    #if Vagrant.has_plugin?
        config.omnibus.chef_version = 'latest'
    #end

    and

    config.vm.synced_folder "../data", "/vagrant_data"
  7. Create ../data with index.php
  8. Add attributes/default.rb
    default['xdebug-box']['document_root'] = '/vagrant_data'

    default['xdebug']['directives'] = {
        "remote_autostart" => 1,
        "remote_connect_back" => 1,
        "remote_enable" => 1,
        "remote_log" => '/tmp/remote.log'
    }

    default['xdebug']['config_file'] = '/etc/php5/apache2/conf.d/xdebug.ini'
  9. Add templates/default/apache2.conf.erb
    <VirtualHost *:80>
        ServerAdmin <%= node['apache']['contact'] %>
        ServerName xdebug.local

        DocumentRoot <%= node['xdebug-box']['document_root'] %>

        <Directory <%= node['xdebug-box']['document_root'] %>>
            AllowOverride All
            Order allow,deny
            Allow from All
            Require all granted
        </Directory>
        RewriteEngine On

        ErrorLog <%= node['apache']['log_dir'] %>/error.log

        LogLevel warn

        CustomLog <%= node['apache']['log_dir'] %>/access.log combined
        ServerSignature Off
    </VirtualHost>
  10. Add templates/default/xdebug.ini.erb
    ; configuration for php xdebug module
    zend_extension = "xdebug.so"
    xdebug.remote_enable = 1
    xdebug.remote_connect_back = 1
    xdebug.remote_port = 9000
    xdebug.remote_handler = "dbgp"
    xdebug.profiler_enable = 0
    xdebug.profiler_enable_trigger = 1
  11. Add recipes/default.rb
    #
    # Cookbook Name:: xdebug-box
    # Recipe:: default
    #

    include_recipe 'apt'

    include_recipe 'apache2'
    include_recipe 'apache2::mpm_prefork'

    include_recipe 'apache2::mod_php5'

    package "php5-xdebug" do
      action :install
    end

    template "#{node['php']['ext_conf_dir']}/xdebug.ini" do
      # Overwrite xdebug.ini
      # (Temporary workaround for https://github.com/opscode-cookbooks/php/issues/108)
      source "xdebug.ini.erb"
      owner "root"
      group "root"
      mode "0644"
      action :create
      notifies :restart, resources("service[apache2]"), :delayed
    end

    apache_site '000-default' do
      enable false
    end

    web_app "xdebug-box" do
       template 'apache2.conf.erb'
       server_name node['xdebug-box']['hostname']
    end
  12. Install vagrant omnibus plugin
    vagrant plugin install vagrant-omnibus
  13. Bring machine up. It can take some time
    ➜  xdebug-box git:(master) ✗ vagrant up
  14. Set a breakpoint in the code and run the page in the browser


You can find the code on my GitHub

Summarizing Backend vs Frontend Page Performance using Scalding

Some time ago I started collecting the page performance data using boomerang.js. As a data storage I simply used access logs. The result is quite predictible. Most of the load time is spent on frontend (except page 10.html, where are some computation).

Firstly I wrote a Cascading job.

Later I rewrote it for Scalding. As you can see it, it is much shorter and more concise.

Choose the right tools for your Magento project

Magento can be hard. We all know it. We can make your work easier by picking the right tools. I wanted to share with you the stuff I am using in my last project. And which, IMO, should the MUST-BE in every Magento installation.

PHP-Error

https://github.com/panrafal/PHP-Error

Better error display.

My hack for it https://gist.github.com/piotrbelina/10d5544c5fc872ec2a77

Aoe Advanced Template Hints

http://www.fabrizio-branca.de/magento-advanced-template-hints-20.html

Very informative hints. Easy to switch on / off – just add ?ath=1 to URL.

Magneto Debug

https://github.com/madalinoprea/magneto-debug

Just like Symfony debug toolbar.

Aoe Magento Profiler

http://www.fabrizio-branca.de/magento-profiler.html

A wrapper for default magento profiler. Easy to switch on / off – just add ?profile=1 to URL. Scoping included

Magento Advanced Logging

https://github.com/magento-hackathon/Logger

My today’s discovery. Log produces for it is a lot more informative. + email and other notifiers.

2013-05-24T13:06:01+00:00 ERR (admin): GET /index.php/admin/system_config/state/key/0bbb6c85b783912518cf1d68640ba8905f5b31b4da751ba8ff38a410273f20ce/?isAjax=true&amp;container=logger_advanced&amp;value=1&amp;form_key=NVYdQ45Ser9dbmGA
REQUEST: GET|{"isAjax":"true","container":"logger_advanced","value":"1","form_key":"NVYdQ45Ser9dbmGA"}
TIME: 0.180086s
ADDRESS: 127.0.0.1
USER AGENT: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.22 (KHTML, like Gecko) Ubuntu Chromium/25.0.1364.160 Chrome/25.0.1364.160 Safari/537.22
FILE: src/app/code/core/Mage/Core/functions.php:247
Warning: str_replace() expects at least 3 parameters, 0 given in /home/.../src/app/code/core/Mage/Core/Model/App.php on line 350

Vs standard magento log:

2013-05-24T13:05:12+00:00 ERR (3): Warning: str_replace() expects at least 3 parameters, 0 given in /home/.../src/app/code/core/Mage/Core/Model/App.php on line 350

Aoe Scheduler

http://www.fabrizio-branca.de/magento-cron-scheduler.html

Last, but not the least. Magento crontab preview.

Using PHPUnit with Gaufrette to unit test IO-dependent behaviour

Recently I wanted to test some class which uses heavily IO. I was refactoring some legacy code depending on file_get_contents and other file functions. To allow easy testing I added Gaufrette library and replaced all native PHP functions operating on IO.

<?php

use Gaufrette\Filesystem;

class Generator
{
    /**
     * @var Filesystem
     */

    protected $filesystem;

    public function __construct(Filesystem $filesystem)
    {
         $this->filesystem = $filesystem;
    }
   
    public function someOperation()
    {
         $content = $this->filesystem->get('myfile')->getContent();
         // ... some operation
         return $result;
    }
}

Unit test for our class

<?php

use Gaufrette\Adapter\InMemory;
use Gaufrette\Filesystem;

class GeneratorTest extends \PHPUnit_Framework_TestCase
{
    public function testSomeOperation()
    {
         $adapter = new InMemory(array('myfile' => 'content'));
         $filesystem = new Filesystem($adapter);
         $generator = new Generator($filesystem);

         $result = $generator->someOperation();

         // assert something with result
    }
}

When using our generator in the code, we can easily change the adapter to support local filesystem, remote and cloud ones (full list of adapters here).

<?php

use Gaufrette\Adapter\Local; // using local filesystem
use Gaufrette\Filesystem;

class GeneratorRunner
{
    public function run()
    {
         $adapter = new Local('directory');
         $filesystem = new Filesystem($adapter);
         $generator = new Generator($filesystem);

         $result = $generator->someOperation();

    }
}

Of course you can use Dependency Injection which allows for even simpler adapter change.

parameters:
  generator.class
: Generator
  filesystem.class
: Gaufrette\Filesystem
  adapter.class
:   Gaufrette\Adapter\Local
  adapter.arguments
: ["directory"]

services
:
  generator
:
    class
: %generator.class%
    arguments
: ["@filesystem"]
  filesystem
:
    class
: %filesystem.class%
    arguments
: ["@adapter"]
  adapter
:
    class
: %adapter.class%
    arguments
: %adapter.arguments%
// ...
$generator = $container->get('generator');
$generator->someAction();

Magento: Enabling modules via local.xml

I wanted to enable some modules only locally, without the possibility to use them on production. This would eliminate a vulnerability of leaking debug data. As an example I will use Magneto Debug module.

To enable a module only locally and keep it turned off anywhere else.

  1. In app/etc/modules/Magento_Debug.xml delete the line
    <active>true</active>
  2. In app/etc/local.xml add the following lines
    <modules>
        <Magneto_Debug>
            <active>true</active>
        </Magneto_Debug>
    </modules>

Deleting the line from app/etc/modules/Magento_Debug.xml is required, because Magento firstly loads app/etc/local.xml and then overloads its content.

Magento: Debugging Webservices

During an audit I had to check with a debugger how one of Magento Webservices works. I found a post on Troubleshooting Magento Web Service using Python so I could easily manipulate with SOAP requests. To debug the code in IDE I added the cookie with XDebug session id.

from suds.client import Client
from suds.xsd.doctor import ImportDoctor, Import
 
d = ImportDoctor(Import('http://schemas.xmlsoap.org/soap/encoding/'))
c = Client('http://www.magento.local/api/v2_soap?wsdl=1', doctor=d, headers={ 'Cookie': 'XDEBUG_SESSION=netbeans-xdebug', })
sid = c.service.login('user', 'pass')
 
print c.service.catalogProductInfo(sid, 633)

Resources

Symfony 2: Sending a file to download from controller

Here is a snippet how to send a file to download. It creates a zip archive and sends it.

<?php

namespace Acme\DemoBundle\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use ZipArchive;

class MyController extends Controller
    /**
     * @Route("/", name="index")
     */

    public function indexAction()
    {
        $entity = $this->getEntity();

        $index = $this->renderView('Acme:Demo:index.html.twig');
       
        $archive = new ZipArchive();
        $archive->open($this->get('kernel')->getRootDir() . '/../web/zip/' . $entity->getHash() . '.zip', ZipArchive::CREATE);
        $archive->addFromString($entity->getHash() . '.php', $index);  
       
        $response = new Response(file_get_contents($archive->filename));
       
        $d = $response->headers->makeDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $entity->getHash() . '.zip');
        $response->headers->set('Content-Disposition', $d);
       
        $archive->close();
       
        return $response;
    }

Resources

HttpFoundation Component documentation: Downloading Files