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 :
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 :
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 !
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 }}
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 :
Log into server in command line with the handy shortcut defined in deploy.rb :
cap ssh
Connect to github
ssh git@github.com
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 :
Remove everything on the server :
cap ssh
rm -rf /home/johndoe
Reinitialize the database
echo "drop database DatabaseName;" | mysql -u root
You are done !
Some stuff is probably missing, do not hesitate to comment below.
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 :
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 :
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 :