May 12th, 2008
As you may know Capistrano is a great tool "originally written to ease the pain of deploying Rails applications". If you are not familiar with it take a look at it's getting started guide.
Even if Rails applications development is the primary way that people use Capistrano it can be used for a lot more than that.
Let me show you how you can use it for easy-and-with-no-pain deployment of symfony project.
First cd to your project and run (I assume that you have Capistrano installed allready)
to capify your project . This will create two files - Capify and config/deploy.rb. We'll use the second one for our symfony related tasks.
So this is how the modifed deploy.rb file that I use to deploy my symfony projects looks like :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
|
set :application, "mysfapp"
set :repository, "http://path/to/the/version/control/repository"
# If you aren't deploying to /u/apps/#{application} on the target
# servers (which is the default), you can specify the actual location
# via the :deploy_to variable:
set :deploy_to, "/path/to/project/#{application}"
# If there's no access to the repository from the production server, deploy via uploading tarball to the server
#set :deploy_via, :copy
# If you aren't using Subversion to manage your source code, specify
# your SCM below:
# set :scm, :subversion
role :app, "codingspree.net"
role :web, "codingspree.net"
# role :db, "your db-server here", :primary => true
set :user, "codingspree"
# path to php executable
set :php, "/usr/local/php5/bin/php5"
# symfony application name (used for migrations)
set :sf_app, "frontend"
namespace (:deploy) do
desc <<-DESC
[internal] Overriding original task to fit to symfony project needs
DESC
task :finalize_update, :except => { :no_release => true } do
run "chmod -R g+w #{latest_release}" if fetch(:group_writable, true)
run <<-CMD
rm -rf #{latest_release}/log &&
ln -s #{shared_path}/log #{latest_release}/log
CMD
run <<-CMD
rm -rf #{latest_release}/cache &&
ln -s #{shared_path}/cache #{latest_release}/cache
CMD
stamp = Time.now.utc.strftime("%Y%m%d%H%M.%S")
asset_paths = %w(images css js).map { |p| "#{latest_release}/web/#{p}" }.join(" ")
run "find #{asset_paths} -exec touch -t #{stamp} {} ';'; true", :env => { "TZ" => "UTC" }
end
desc <<-DESC
Overriding original task to exclude restart
DESC
task :default do
update
end
desc <<-DESC
Overriding original task to use symfoy migrations (via sfMigrationsLightPlugin)
DESC
task :migrations do
update
sf.migrate
end
after "deploy:update", 'deploy:customize'
desc <<-DESC
All custom tasks will be here
DESC
task :customize do
# custmize it here
sf.symlinks
sf.remove_dev_environments
# clear cache
sf.cc
end
end
namespace (:sf) do
desc <<-DESC
Run the "symfony migrate" task
DESC
task :migrate do
run "cd #{current_path} && #{php} symfony migrate #{sf_app}"
end
desc <<-DESC
Run the "symfony cc" task
DESC
task :cc do
run "cd #{current_path} && rm -rf cache/*"
end
desc <<-DESC
Create symlink to symfony specific targets
DESC
task :symlinks do
# symlink to database.yml
run "rm -rf #{current_path}/config/databases.yml"
run "ln -s #{shared_path}/databases.yml #{current_path}/config/databases.yml"
# symlink to config.php
run "rm -rf #{current_path}/config/config.php"
run "ln -s #{shared_path}/config.php #{current_path}/config/config.php"
# symlink to sf data dir
run "rm -rf #{current_path}/web/sf"
run "ln -s /path/to/sf/data/dir #{current_path}/web/sf"
# symlink to uploads
run "rm -rf #{current_path}/web/uploads"
run "ln -s #{shared_path}/uploads #{current_path}/web/uploads"
end
desc <<-DESC
Remove DEV environments
DESC
task :remove_dev_environments do
run "rm -rf #{current_path}/web/*_dev.php"
end
desc <<-DESC
Disable symfony application
DESC
task :disable do
run "cd #{current_path} && #{php} symfony disable #{sf_app} prod"
end
desc <<-DESC
Enable symfony application
DESC
task :enable do
run "cd #{current_path} && #{php} symfony enable #{sf_app} prod"
end
end |
As you can see most of it is usual Capistrano configuration (user, repository, application, etc.) so I'll explain only the symfony related stuff.
1
2
|
# path to php executable
set :php, "/usr/local/php5/bin/php5" |
This variable is used to specify where PHP5 cli is placed (you know not all hostings are using PHP5 as default).
1
2
|
# symfony application name (used for migrations)
set :sf_app, "frontend" |
Here we specify the name of one of our symfony project applications. It will be needed to run migrations (you ARE using
sfPropelMigrationsLightPlugin allready, right?).
In the next section of the deploy.rb file I overrided few of the original tasks from deploy namespace to fit to my needs - finalize_update to set symlinks to symfony cache and log directories, the default task to exclude restart because I don't need it, migrations to set it to use symfony migrations and at the end there is one hook task that will be executed after deploy:update -it's called customize and I'll place all custom tasks there.
The last part of the file is
sf namespace that includes some symfony related tasks as migrate, cc, symlinks, remove_dev_environments, disable, enable and so on. As you see you can wrap any symfony task that you have to execute on the production server with Capistrano task.
All you need now is to set a little bit the production server (run cap deploy:config, symlink document root to /path/to/project/mysfapp/current, place relevant database.yml and config.php in /path/to/project/mysfapp/shared, etc) and you are ready to run
Thats it! Easy one command deployment! With migrations (cap deploy:migrate)! Hope it's explained clear enough (if not place your questions in the comments).
September 11th, 2007
This is a simple how-to that describes adding callbacks for Propel models in symfony project
(like those usefull methods in ActiveRecord::Callbacks module).
Step 1 : Enable Propel behaviors in config/propel.ini
|
propel.builder.AddBehaviors = true // Default value is false |
Step 2 : Register the callback method and define it in model class - for example
lib/model/Page.php
1
2
3
4
5
6
7
8
9
|
class PageTab extends BasePageTab
{
function preSave(BaseObject $object, $con)
{
}
}
sfMixer::register('BasePageTab:save:pre', array('PageTab', 'preSave')); |
Step 3 : Rebuild the model
|
$ symfony propel-build-model |
There is much more detailed article on this topic by
François Zaninotto called
"Understanding Behaviors"
September 10th, 2007
A very common error when you first start to write an application in the MVC way is bypassing some of the components or just not using it as it should.
This problem and an example how to do it in the "right way" are described in a one of Jamis Buck's posts called Skinny Controller, Fat Model.
Here is a cover-up version of the code from that post ported to symfony/PHP...
Before (or how-NOT-to-do-it example) :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
<!-- listSuccess.php -->
<?php
foreach ($path as $breadcrumb=>$name)
{
?>
<a href="<?=url_for($breadcrumb)?>"><?=__($name)?></a> /
<?
}
foreach ($people as $p)
{
?>
<div id="person-<?=$p->getId()?>">
<span class="name">
<?=$p->getLastName()?>, <?=$p->getFirstName()?>
</span>
<span class="age">
<?= (date('Y') - date('Y', strtotime($p->getBirthdate()))) ?>
</span>
</div>
<?
}
?> |
1
2
3
4
5
6
7
8
9
10
11
12
|
# actions.php
class personsActions extends sfActions
{
public function executeList()
{
$c = new Criteria;
$c->add(PersonPeer::ADDED_AT, date('Y-m-d'), Criteria::GREATER_EQUAL);
$c->add(PersonPeer::DELETED, false);
$this->people = Person::doSelect($c);
}
} |
1
2
3
4
|
# PersonPeer.php
class PersonPeer extends BasePersonPeer
{
} |
After (or do-it-in-this-way example) :
1
2
3
4
5
6
7
8
9
|
<!-- listSuccess.php -->
<?php echo breadcrumbs($path) ?>
<?php foreach ($people as $p): ?>
<div id="person-<?php echo $p->getId()?>">
<span class="name"><?php echo $p->getFullName()?></span>
<span class="age"><?php echo $p->getAge()?></span>
</div>
<? endforeach;?> |
1
2
3
4
5
6
7
8
|
# actions.php
class personsActions extends sfActions
{
public function executeList()
{
$this->people = PersonPeer::findRecent();
}
} |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
# PersonPeer.php
class PersonPeer extends BasePersonPeer
{
public function findRecent()
{
$c = new Criteria;
$c->add(self::ADDED_AT, date('Y-m-d'), Criteria::GREATER_EQUAL);
$c->add(self::DELETED, false);
return self::doSelect($c);
}
}
# Person.php
class Person extends BasePerson
{
public function getFullName()
{
return $this->getLastName() . ', ' . $p->getFirstName();
}
public function getAge()
{
return (date('Y') - date('Y', strtotime($this->getBirthdate())));
}
} |