samedi 22 octobre 2011

A table printer for the Symfony2 console

Here is a custom class for printing nice tables in the Symfony2 console

<?php
namespace Acme\DemoBundle\Util;

use Symfony\Component\Console\Output\OutputInterface;

class ConsolePrinter
{
    
    static private $channel = null;
    
    // =========================================================================
    //                                  FUNCTIONS
    // =========================================================================
    
    /**
     * public static function printT($body, $head = null)
     * 
     * This function performs the full print of the table
     * 
     * @author      Antoine Durieux
     * 
     * @version     1.0
     * 
     * @param       array       $line           The line to be printed
     * @param       array       $columnWidths   The widths of the columns
     * @return      string                      The resulting string 
     */
    public static function printT($body, $head = null)
    {
        // ---------------------------------------------------------------------
        // 1. Find column widths to use
        // ---------------------------------------------------------------------
        $columnWidths = self::computeColumnWidths($body,$head);
        
        // ---------------------------------------------------------------------
        // 2. Print header
        // ---------------------------------------------------------------------
        if($head !== null)
        {
            self::printConsole(self::printBlankLine($columnWidths));
            self::printConsole(self::printLine($head,$columnWidths));
        }
        
        // ---------------------------------------------------------------------
        // 3. Print body
        // ---------------------------------------------------------------------
        self::printConsole(self::printBlankLine($columnWidths));
        foreach($body as $index => $row)
        {
            self::printConsole(self::printLine($row,$columnWidths));
        }
        self::printConsole(self::printBlankLine($columnWidths));
    }
    
    /**
     * private static function computeColumnWidths($body,$head = null)
     * 
     * This function computes the sizes of the columns depending on the size of
     * what they will contain.
     * 
     * @author      Antoine Durieux
     * 
     * @version     1.0
     * 
     * @param       array       $body           The body of the table
     * @param       array       $head           The header of the table
     * @return      array                       The maximum size of a columnt 
     */
    private static function computeColumnWidths($body,$head = null)
    {
        $columnWidths = array();
        // In case we have no head
        if($head!==null)
        {
            $columnWidths = array_map('mb_strlen',$head);
        }
        else
        {
            $columnWidths = array_map('mb_strlen',$body[key($body)]);
        }
        foreach($body as $index => $row)
        {
            foreach($row as $jndex => $value)
            {
                $columnWidths[$jndex] = max(mb_strlen($value),$columnWidths[$jndex]);
            }
        }
        return $columnWidths;
    }
    
    /**
     * private static function printBlankLine($columnWidths)
     * 
     * This function returns a string that corresponds to a decorating line.
     * 
     * @author      Antoine Durieux
     * 
     * @version     1.0
     * 
     * @param       array       $columnWidths   The widths of the columns
     * @return      string                      The resulting string 
     */
    private static function printBlankLine($columnWidths)
    {
        $line = '+';
        foreach($columnWidths as $index => $value)
        {
            $line .= str_repeat('-',$columnWidths[$index]+2).'+';
        }
        return $line;
    }
    
    /**
     * private static function printLine($line,$columnWidths)
     * 
     * This function returns a string that corresponds to a regular line of the 
     * table.
     * 
     * @author      Antoine Durieux
     * 
     * @version     1.0
     * 
     * @param       array       $line           The line to be printed
     * @param       array       $columnWidths   The widths of the columns
     * @return      string                      The resulting string 
     */
    private static function printLine($line,$columnWidths)
    {
        $string = '|';
        foreach($line as $index => $value)
        {
            $string .= ' '.$value.str_repeat(' ',$columnWidths[$index]-mb_strlen($value)).' |';
        }
        return $string;
    }
 
    /**
     * public static function printConsole($string)
     * 
     * This function prints a line in the console
     * 
     * @author      Antoine Durieux
     * 
     * @version     1.0
     * 
     * @param       string       $string           The string to print in the console
     */
    public static function printConsole($string)
    {
        self::$channel->writeln($string);
    }
    
    // =========================================================================
    //                                   SETTERS
    // =========================================================================
    
    /**
     * public static function setChannel(OutputInterface $channel)
     * 
     * This function sets the outputting channel.
     * 
     * @author      Antoine Durieux
     * 
     * @version     1.0
     * 
     * @param OutputInterface $channel 
     */
    public static function setChannel(OutputInterface $channel)
    {
        self::$channel = $channel;
    }
    
}



And here is a use example in a Command file of Symfony2 :
$head = array('ID', 'Name');
$body = array(
    array('1', 'John Doe'),
    array('2', 'Joe Smith'),
 );
ConsolePrinter::setChannel($output);
ConsolePrinter::printT($body, $head);
And its result :
+----+-----------+
| ID | Name      |
+----+-----------+
| 1  | John Doe  |
| 2  | Joe Smith |
+----+-----------+
Enjoy !

mercredi 19 octobre 2011

Setup vhosts for Symfony2 development in MAMP

Let's say you are developing your Symfony2 website in MAMP, but typing "http://localhost/Symfony/web/app_dev.php/login" doesn't make you happy. You want to shorten it to somthing like : "http://mywebsite/login".

Get rid of the "app_dev.php"

To do so, you just need to modify the .htaccess file of the Symfony/web folder :
  • Check that the URL rewriting module is available
  • Turn the rewrite engine on
  • Define a rewriting rule

<IfModule mod_rewrite.c>
    Options +FollowSymlinks
    RewriteEngine On
    
    # Explicitly disable rewriting for front controllers
    RewriteRule ^app_dev.php - [L]
    RewriteRule ^app.php - [L]
    
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteRule ^(.*)$ app_dev.php [QSA,L]
</IfModule>


A rewrite rule is bascally a regex that Apache applies to every incoming requested URL.
NB : the last rewrite rule needs to be changed before deploying to production.

Turn the http://localhost/Symfony/web into http://mywebsite

First, edit /Applications/MAMP/cong/apache/httpd.conf and add a new virtual host to MAMP : make sure that virtual hosts are enabled on your server : you should find that somewhere close to the bottom of the file :
NameVirtualHost *

Then you can append at the very bottom :
<VirtualHost *>
   DocumentRoot "/Applications/MAMP/htdocs/Symfony/web"
   ServerName mywebsite
</VirtualHost>

This basically says that when the mywebsite server will be called, the request will be redirected to /Applications/MAMP/htdocs/Symfony/web.
Now, edit the hosts for your computer : edit /etc/hosts and append at the end of the file :
127.0.0.1   mywebsite

This basically says that typing http://mywebsite will be redirected to the internal IP 127.0.0.1.
You are done !

Resources

dimanche 9 octobre 2011

Generate specific error pages with Symfony2

404 and 500 errors

Symfony2 allows to customize error pages by overriding Twig templates as decribed here. You can override the Twig templates for a range of typical cases like :
  • error.html.twig
  • exception.html.twig
  • error404.html.twig
  • and various other formats
From the controller, you can render a 500 error template by calling :
throw new \Exception('There is something wrong');
Or for a 404 error :
throw $this->createNotFoundException('The product does not exist');

More customized errors

But if you want to render, let's say a 402 exception, you have to use :
// At the beginnig of the file
use Symfony\Component\HttpKernel\Exception\HttpExcetion;
// In the controller
throw new HttpException(402,'There is something wrong');
If you want to display this custom message in the template, you have to add :
// Code of the error
{{ status_code }} 
// Standard name of the error
{{ status_text }}
// Text you specified when throwing the exception
{{ exception.message|e|replace({"n": '<br/ >'})|format_file_from_text }}

Other resources

vendredi 7 octobre 2011

Deploying a Symfony2 project using Capistrano

Here is a tutorial to deploy a Symfony2 application on a remote server through github thanks to Capifony.

Thanks a lot to @qpleple for the help !

Get the stuff

Just in case you start from scratch on a mac OS X :
  1. Install Xcode from the Application Disc (install everything, including UNIX developer tools)
  2. Download & install Macports
  3. Install/update Ruby through macports :
    sudo port install ruby
  4. Install/update RubyGems
    sudo gem update --system
  5. Install Capifony
    sudo gem install capifony


Configure Capifony

Setup the capifony project as explained here.
Here is a sample (and more comprehensive) deploy.rb file (developed by @qpleple, annotated and reorganised by me) :
# ==============================================================================
#                                CONFIGURATION
# ==============================================================================

# ------------------------------------------------------------------------------
# 1. Servers configuration
# ------------------------------------------------------------------------------
# IP Adress of the servers to push the code to
set :domain,      "12.34.56.78"
set :application, "JohnDoeAppication"
set :serverName,  domain

# Front servers, nginx, HTTP server, Apache/etc
role :web,        domain
# Web server, where the code will go
role :app,        domain
# Database server       
role :db,         domain, :primary => true

set :deploy_to,   "/home/johndoe"

# ------------------------------------------------------------------------------
# 2. Security configuration
# ------------------------------------------------------------------------------
# Security : specification of the specific ssh key of the server
ssh_options[:keys] = %w(/Users/johndoe/.ssh/johndoe.pem)
ssh_options[:forward_agent] = true

# ------------------------------------------------------------------------------
# 3. Github and SCM configuration
# ------------------------------------------------------------------------------
# Repository path
set :repository,  "git@github.com:johndoe/Symfony.git"
# SCM type
set :scm,         :git
# Fetch only master branch
set :branch, "master"
# Use local keys to pull from github
set :scm_verbose, false
# Will keep a local git repo : only diffs will be uploaded
set :deploy_via, :remote_cache
# The number of releases which will remain on the server
set  :keep_releases,  3
# Automates the backup process
after "deploy:update", "deploy:cleanup" 

# ------------------------------------------------------------------------------
# 4. Project configuration
# ------------------------------------------------------------------------------
# Set some paths to be shared between versions : images, config..
set :shared_files,    ["app/config/parameters.ini"]
set :shared_children, [app_path + "/logs", web_path + "/uploads", "vendor"]
# Don't check vendors after each deploy : it has to be done manually
set :update_vendors, false

# ------------------------------------------------------------------------------
# 5. Capifony parameters
# ------------------------------------------------------------------------------
set :user,       "root"
set :use_sudo,   false
set :app_path,    "app"


after "deploy" do
    # Make app.php the front controller (and not app_dev.php)
    # in order not to have to change the htaccess manualyy everytime
    run "sed -i 's/app_dev/app/' #{deploy_to}/current/web/.htaccess"
end


# ==============================================================================
#                                     TASKS
# ==============================================================================

# ------------------------------------------------------------------------------
# Automated connection to the server
# ------------------------------------------------------------------------------

task :ssh do
    # To ssh into the prod server
    system("chmod 600 #{ssh_options[:keys]}")
    system("ssh -A -i #{ssh_options[:keys]} root@#{serverName}")
end

# ------------------------------------------------------------------------------
# First deploy
# ------------------------------------------------------------------------------

task :firstdeploy do
    # Make task fail if it is not the first deploy
    run "[ -d #{deploy_to} ] && exit 1 || echo 'ok'"
    
    # Install missing packages:
    # git-core: to get the code from github
    # php5-mysql: to make php able to talk to mysql (mysqli, pdo_mysql)
    # php5-sqlite: required by the profiler
    # php5-curl: CURL library
    run "apt-get install --yes git-core php5-mysql php5-sqlite php5-curl"
    
    # Activate url rewriting apache module
    run "a2enmod rewrite"
    
    # Create directory arborescence : creation of a shared folder, etc...
    deploy.setup
    
    # Permissions
    # Create new permission group
    run "egrep -i '^www-pub' /etc/group > /dev/null || groupadd www-pub"
    # Add the www-data user to the www-pub group
    run "usermod -a -G www-pub www-data"
    # Add the root user to the www-pub group
    run "usermod -a -G www-pub root"
    # Transfers the ownership of Home to the www-pub group
    run "chown -R root:www-pub #{deploy_to}"
    # Setup permissions for files and folders
    run "find #{deploy_to} -type d -exec chmod 2775 {} ;"
    run "find #{deploy_to} -type f -exec chmod 0664 {} ;"
    
    # Symlink from apache root dir to app web dir
    # run "rm -rf /var/www"
    # run "ln -s #{deploy_to}/current/web /var/www"

    set :mysql_password, Capistrano::CLI.ui.ask("Enter MySQL database password: ")

    # File parameters.ini
    template = <<-EOF
[parameters]
    database_driver="pdo_mysql"
    database_host="localhost"
    database_name="JohnDoeDatabase"
    database_user="root"
    database_password = #{mysql_password}
    mailer_transport="smtp"
    mailer_host="localhost"
    mailer_user=""
    mailer_password=""
    locale="en"
    secret            = #{Capistrano::CLI.ui.ask("Secret token: ")}
EOF
    run "mkdir -p #{shared_path}/app/config"
    put ERB.new(template).result(binding), "#{shared_path}/app/config/parameters.ini"

    # Tell Capistrano to install vendors 
    set :update_vendors, true
    
    # Make a new release, fetch code, install vendors
    deploy.default
    
    # Set mysql root password
    run "mysqladmin -u root password #{mysql_password} || echo 'password already set'"
    # Create database
    run "echo 'CREATE DATABASE JohnDoeDatabase CHARACTER SET utf8 COLLATE utf8_general_ci' | mysql -p#{mysql_password}"
    # Create schema
    symfony.doctrine.schema.create
    
end

# ------------------------------------------------------------------------------
# Clear distant cache
# ------------------------------------------------------------------------------

task :cc do
    run "rm -rf #{deploy_to}/current/app/cache"
end


Setup your server

You first have to make your server able to connect to the github repository : github has to be added to the know hosts list of the server. In order to do so :
  1. Log into server in command line with the handy shortcut defined in deploy.rb :
    cap ssh 
  2. Connect to github
    ssh git@github.com
  3. Quit server


Setup your computer

When you will try to push code to the server, the github server will follow the ssh challenge to the computer from which you are pushing. You thus have to have your id_rsa identity loaded in the ssh-agent of your computer at this time. You can display what are the currently loaded identities with :
ssh-add -l
If the key is missing :
ssh-add ~/.ssh/id_rsa
This has to be done after every reboot.


Deploy

Launch first deploy :
cap firstdeploy

If it crashes at some point :
  1. Remove everything on the server :
    cap ssh
    rm -rf /home/johndoe
  2. Reinitialize the database
    echo "drop database DatabaseName;" | mysql -u root



You are done !

Some stuff is probably missing, do not hesitate to comment below.


References

mardi 4 octobre 2011

OneToMany relationship in Doctrine2 and Symfony2 for dummies

It may seem obvious to a lot, but ...

The classes

In a forum, there are Authors. These authors can post messages. Every Message has one and only one Author, and an Author can write many Messages : this is typically a OneToMany relationship.
Here are the equivalent classes :
/**
 * @ORM\Entity
 * @ORM\Table(name="Author")
 * @ORM\Entity(repositoryClass="Acme\DemoBundle\AuthorRepository")
 */
class Author
{
 
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     * @var int
     */
    protected $id;
 
    /**
     * @ORM\OneToMany(targetEntity="Message", mappedBy="author", cascade={"persist", "remove"})
     */
    protected $messages;
    
    /**
     * Add message
     *
     * @param Acme\DemoBundle\Entity\Message $message
     */
    public function addMessage(Acme\DemoBundle\Entity\Message $message)
    {
        $this->mesages[] = $message;
        $message->setAuthor($this);
        return $this;
    }
    
    /**
     * Get messages
     *
     * @return Doctrine\Common\Collections\Collection 
     */
    public function getMessages()
    {
        return $this->messages;
    }

    
    public function __construct()
    {
        $this->messages = new \Doctrine\Common\Collections\ArrayCollection();
    }
}


/**
 * @ORM\Entity
 * @ORM\Table(name="Message")
 * @ORM\Entity(repositoryClass="Acme\DemoBundle\Repository\MessageRepository")
 */
class Message
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     * @var int
     */
    protected $id;
    
    /**
     * @ORM\ManyToOne(targetEntity="Author", inversedBy="messages", cascade={"persist", "update"})
     * @ORM\JoinColumn(name="author_id", referencedColumnName="id", onDelete="cascade", onUpdate="cascade")
     * @var Acme\DemoBundle\Entity\Author
     */
    protected $author;
    
}
 

The issue


Now, let's say you want to transfer messages from author 2 to author 1 for some reason. Here is how to do it step by step :
// Get the entities
$Author1 = $authorRepository->find($idAuthor1);
$Author2 = $authorRepository->find($idAuthor2);

// Retrieve messages
$messagesOfAuthor2 = $Author2->getMessages();
Here is a visual description of what has happened :

State 0 :
We transfer each of the messages :
// Move message
foreach($messagesOfAuthor2 as $message)
{
    $Author1->addMessage($message);
    // Leads to state 1
    $Author2->getMessages()->removeElement($message);
    // Leads to state 2
}

State 1 :

State 2 :

Note the deletion of 1 link in the removeElement function, and the addition of 2 links in the addMessage function.
We can now remove the Author 2 and flush the EntityManager :
$entityManager->remove($Author2);

$entityManager->flush();

dimanche 2 octobre 2011

Doctrine2 Extensions : Tree

Here are some notes on the Doctrine2 Tree behavioral extensions :

Resources :




Tips & tricks :


The bundle handles everything when it comes to create a new entity in the tree : it sets up the level, changes the left born and the right born, etc...

Be careful when flushing entities that have a relation with nested sets : cascade setup must be adjusted properly.

I think there is no way (yet) to move an entire subtree somewhere else in the tree.

There is a nice way to represent the nested set in html :

$options = array(
            'representationField' => 'name',
            'child' => array(
                'open'=> '<li>',
                'close' => '</li>' ),
            'root' => array(
                'open'=<ul>',
                'close' => '</ul>' )
        );
$html = $rep->childrenHierarchy(null,false,true,$options);
 

The purpose of the 'representationField' parameter is to tell the printer which field to output in the tree.