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

Magento, Composer and Dependency Injection

Recently I found an information about Composer installer for Magento. I explored this a little bit and I managed to include some other packages (Monolog) and Symfony’s bundles into magento installation. To allow easy configuration I included also Symfony’s Dependency Injector.
How to use it?
  1. Define composer.json in root dir
    {
        "minimum-stability"
    : "dev",
        "require"
    : {
            "monolog/monolog"
    : "dev-master",
        "magento-hackathon/magento-composer-installer"
    : "dev-master",
            "symfony/dependency-injection"
    : "dev-master",
            "symfony/yaml"
    : "dev-master",
            "symfony/config"
    : "dev-master",
            "symfony/monolog-bundle"
    : "dev-master",
            "mlehner/gelf-php"
    : "dev-master"
        },
        "repositories"
    : [
            {
                "type"
    : "composer",
                "url"
    : "http://packages.firegento.com"
            }
        ],
        "extra":{
            "magento-root-dir"
    : "src/"
        }
    }
  2. Download composer
  3. mkdir bin
    curl -s https://getcomposer.org/installer | php -- --install-dir=bin
    bin/composer.phar install
  4. Create Your_Core_Model_Config
    <?php
    use Symfony\Component\DependencyInjection\ContainerAwareInterface;
    use Symfony\Component\DependencyInjection\ContainerBuilder;
    use Symfony\Component\Config\FileLocator;
    use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
    use Symfony\Component\HttpKernel\DependencyInjection\MergeExtensionConfigurationPass;
    use Symfony\Component\HttpKernel\DependencyInjection\AddClassesToCachePass;

    class Your_Core_Model_Config extends Mage_Core_Model_Config
    {
        /**
         *
         * @var Symfony\Component\DependencyInjection\Container
         */

        protected $container = null;
       
        /**
         * Get model class instance.
         *
         * Example:
         * $config->getModelInstance('catalog/product')
         *
         * Will instantiate Mage_Catalog_Model_Mysql4_Product
         *
         * @param string $modelClass
         * @param array|object $constructArguments
         * @return Mage_Core_Model_Abstract
         */

        public function getModelInstance($modelClass='', $constructArguments=array())
        {
            $className = $this->getModelClassName($modelClass);
            if (class_exists($className)) {
                Varien_Profiler::start('CORE::create_object_of::'.$className);
                $obj = new $className($constructArguments);
                if ($obj instanceof ContainerAwareInterface) {
                    $obj->setContainer($this->getContainer());
                }
                Varien_Profiler::stop('CORE::create_object_of::'.$className);
                return $obj;
            } else {
                #throw Mage::exception('Mage_Core', Mage::helper('core')->__('Model class does not exist: %s.', $modelClass));
               return false;
            }
        }
       
        public function getContainer()
        {
            if ($this->container == null) {
                $container = new ContainerBuilder();
               
                $bundles = array (
                    new Symfony\Bundle\MonologBundle\MonologBundle(),
                );
               
                $extensions = array();
                foreach ($bundles as $bundle) {
                    if ($extension = $bundle->getContainerExtension()) {
                        $container->registerExtension($extension);
                        $extensions[] = $extension->getAlias();
                    }
                }
               
                foreach ($bundles as $bundle) {
                    $bundle->build($container);
                }
               
                $loader = new YamlFileLoader($container, new FileLocator($this->getOptions()->getEtcDir()));
                $loader->load('services.yml');
                $loader->load('config.yml');
               
                // ensure these extensions are implicitly loaded
                $container->getCompilerPassConfig()->setMergePass(new MergeExtensionConfigurationPass($extensions));

                $container->compile();
               
                $this->container = $container;
               
            }
            return $this->container;
        }
    }

    In app/Mage.php change Mage_Core_Model_Config to Your_Core_Model_Config in run and app methods.

  5. Define app/etc/services.yml:
    parameters:
      monolog.handler.chromephp.class
    : Monolog\Handler\ChromePHPHandler
      monolog.handler.firephp.class
    : Monolog\Handler\FirePHPHandler

    and app/etc/config.yml

    monolog:
        handlers
    :
            main
    :
                type
    : stream
                path
    : "var/log/debug.log"
                level
    : debug
            gelf
    :
                type
    : gelf
                level
    : debug
                publisher
    :
                  hostname
    : localhost
            chromephp
    :
                type
    : chromephp
                level
    : debug
            firephp
    :
                type
    : firephp
                level
    : info
  6. Now you can use it in your classes:
    <?php
    use Symfony\Component\DependencyInjection\ContainerAwareInterface;
    use Symfony\Component\DependencyInjection\ContainerInterface;

    class Your_Module_Model_Something extends Mage_Core_Model_Abstract implements ContainerAwareInterface
    {
        protected $container;

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

        public function log($data)
        {
            $logger = $this->container->get('logger');
            $logger->debug($data);
        }
    }
Why dependency injection is cool you can find in papers below
Resources