.-.
( ( halfmoon
`-`
halfmoon, combined with php-activerecord, is a tiny MVC framework for PHP 5.3 that tries to use the conventions of Ruby on Rails 2.3 wherever possible and reasonable.
It has a similar directory structure to a Rails project, with the root level containing models, views, controllers, and helpers directories. It supports a concept of environments like Rails, defaulting to a development environment which logs things to Apache's error log and displays errors in the browser.
Its URL routing works similarly as well, supporting a catch-all default
route of :controller/:action/:id
and a root URL (/
) route.
Form helpers work similar to Rails. For example, doing this in Rails:
<% form_for :post, @post, :url => "/posts/update" do |f| %>
<%= f.label :title, "Post Title" %>
<%= f.text_field :title, :size => 20 %>
<%= submit_tag "Submit" %>
<% end %>
is similar to this in halfmoon:
<? $form->form_for($post, "/posts/update", array(), function($f) { ?>
<?= $f->label("title", "Post Title"); ?>
<?= $f->text_field("title", array("size" => 20)); ?>
<?= $f->submit_button("Submit") ?>
<? }); ?>
with $form
being an alias to a FormHelper object automatically setup
by the controller. There are other helpers available like $time
,
$html
, etc.
$C
is defined as the current controller object, to access its functions
such as render
.
-
PHP 5.3 or higher with the PDO database extensions you wish to use with php-activerecord (pdo-mysql, pdo-pgsql, etc.).
The
mcrypt
extension is required for using the encrypted cookie session store (see this page for Mac OS X instructions).The
pcntl
extension is required to usescript/dbconsole
. The readline extension is optional, but will improve the use ofscript/console
. Both extensions can be installed on Mac OS X with the same instructions for mcrypt but no extra dependencies (download the PHP tarball for the version thatphp -v
reports, untar,cd ext/{pcntl,readline}; phpize; ./configure; make; make install
, enable in php.ini. -
Apache 1 or 2, with mod_rewrite enabled. Development of halfmoon is done on OpenBSD in a chroot()'d Apache 1 server, so any other environment should work fine.
-
(Optional) Create the root directory where you will be storing everything. halfmoon will do this for you but if you are creating it somewhere where you need sudo permissions, do it manually:
$ sudo mkdir /var/www/example/ $ sudo chown `whoami` /var/www/example
-
Fetch the halfmoon source code into your home directory or somewhere convenient (not in the directory you are setting up halfmoon in):
$ git clone git://github.com/jcs/halfmoon.git
-
Run the halfmoon script to create your skeleton directory at your root directory created in step 1:
$ halfmoon/halfmoon create /var/www/example/ copying halfmoon framework... done. creating skeleton directory structure... done. creating random encryption key for session storage... done. /var/www/example/: total 14 drwxr-xr-x 2 jcs users 512 Feb 15 10:25 config/ drwxr-xr-x 2 jcs users 512 Feb 15 10:20 controllers/ drwxr-xr-x 5 jcs users 512 Mar 15 20:33 halfmoon/ drwxr-xr-x 2 jcs users 512 Mar 15 20:33 helpers/ drwxr-xr-x 2 jcs users 512 Mar 15 20:33 models/ drwxr-xr-x 4 jcs users 512 Feb 13 19:58 public/ drwxr-xr-x 3 jcs users 512 Feb 13 19:58 views/ welcome to halfmoon!
At a later point, halfmoon will be installed system-wide, so that running "
halfmoon create ...
" will work from anywhere. -
Setup an Apache Virtual Host with a DocumentRoot pointing to the public/ directory:
<VirtualHost 127.0.0.1> ServerName www.example.com CustomLog logs/example_access combined # halfmoon will log a few lines for each request (or one # line, or nothing - see config/boot.php) with some # useful information about routing, timing, etc. # # by default these will use php's error_log(), which will # log to the file specified below, but prefixed with # '[error]' and other junk. to log information to a # separate file, create a class that extends and overrides # error(), info(), and warn() methods of \HalfMoon\Log and # use HalfMoon\Config::set_log_handler("YourClass") in your # boot.php file. ErrorLog logs/example_info # this should point to your public directory where index.php # lives to interface with halfmoon DocumentRoot /var/www/example/public/ # try static (cached) pages before dynamic ones DirectoryIndex index.html index.php # uncomment in a production environment, otherwise we are # assuming to be running in development #SetEnv HALFMOON_ENV production # if suhosin is installed, disable session encryption and # bump the maximum id length since we're handling sessions # on our own php_flag suhosin.session.encrypt off php_value suhosin.session.max_id_length 1024 # enable mod_rewrite RewriteEngine on # handle requests for static assets (stylesheets, # javascript, images, cached pages, etc.) directly RewriteCond %{DOCUMENT_ROOT}/%{REQUEST_FILENAME} !-f # route all other requests to halfmoon RewriteRule ^(.*)$ /index.php/%{REQUEST_URI} [QSA,L] </VirtualHost>
-
(Optional) Create the database and its tables and grant permissions. Put those settings in the
config/db.ini
file under the development section.If you are not using a database, or just don't want to use php- activerecord, remove config/db.ini and php-ar will not be initialized, saving you some minor processing time on each request.
By default, halfmoon runs in development mode unless the HALFMOON_ENV environment variable is set to something else (such as via the commented out example above, using apache's SetEnv function).
-
Create models in the
models/
directory according to your database tables.Example
models/Post.php
:<?php class Post extends ActiveRecord\Model { static $belongs_to = array( array("user"), ); } ?>
-
Create controllers in the
controllers/
directory to map urls to actions.Example
controllers/posts_controller.rb
:<?php class PostsController extends ApplicationController { static $before_filter = array("authenticate_user"); public function index() { $this->posts = Post::find("all"); } } ?>
To set variables in the namespace of the view, use
$this->varname
. In the above example,$posts
is an array of all posts and is visible to the view template php file.The index action will be called by default when a route does not specify an action.
Defining a
$before_filter
array of functions will call them before processing the action. If any of them return false (such as one failing to authenticate the user and wanting to redirect to a login page), processing will stop, no other before_filters will be run, and the controller action will not be run. -
Create views in the
views/
directory. By default, controller actions will try to renderviews/*controller*/*action*.phtml
. For example, these URLs:/posts /posts/index
will both call the
index
action inPostsController
, which will renderviews/posts/index.phtml
.A URL of:
/posts/show/1
would map (using the default catch-all route) to the posts controller, calling the
show
action with$id
set to 1, and then renderviews/posts/show.phtml
.Partial views are snippets of HTML that are shared among views and can be included in a view template with render function. Their filenames must start with underscores.
For example, if
views/posts/index.phtml
contained:<?php $C->render(array("partial" => "header_image")); ... ?>
then
views/posts/_header_image.phtml
would be brought in.After a controller renders its view file, it is stored in the
$content_for_layout
variable and theviews/layouts/application.phtml
file is rendered. Be sure to print$content_for_layout
somewhere in that file. -
(Optional) Configure a root route to specify which controller/action should be used for viewing the root (
/
) URL viaconfig/routes.php
:HalfMoon\Router::addRootRoute(array( "controller" => "posts", "action" => "homepage" ));
this uses the same rules as other routes, calling the
index
action if it is not specified.If your site should always present a static page (like a login/splash page) at the root URL, then simply make a public/index.html file to avoid processing through halfmoon. This is handled entirely outside of halfmoon by apache, because of the
mod_rewrite
rule. -
Change or create site-specific and environment-specific settings in the
config/boot.php
script. This can be used to adjust logging, tweak PHP settings, or set global PHP variables that you need.
-
Copy the entire directory tree (/var/www/example in this example) somewhere, setup an Apache Virtual Host like the example above, but use the
SetEnv
apache function to change theHALFMOON_ENV
environment to "production".<VirtualHost ...> ... SetEnv HALFMOON_ENV production ... </VirtualHost>
This will use the database configured in
config/db.ini
under the production group, and any settings you have changed inconfig/boot.php
that are environment-specific (such as disabling logging). -
Verify that your static 404 and 500 pages (in
public/
) have useful content.You may wish to turn halfmoon's logging off completely, instead of the "short" style used by default in production which will only log one line logging the processing time for each request. This can be adjusted in
config/boot.php
:HalfMoon\Config::set_activerecord_log_level("none");
It is also recommended that you enable exception notification e-mails, which will e-mail you a backtrace and some helpful debugging information any time an error happens in your application:
HalfMoon\Config::set_exception_notification_recipient("you@example.com"); HalfMoon\Config::set_exception_notification_subject("[your app]");
Newer versions of PHP include optional FPM support which makes it easy to run halfmoon applications in a secure environment and, when combined with APC, can increase performance by caching PHP code between requests. The halfmoon framework and models will not need to be reloaded and re-parsed on every request.
After installing PHP with FPM support and the APC PECL module, FPM can be configured to chroot to your halfmoon application's root directory, and run the application as a specific user. To extend the configuration of the example Apache configuration above, the relevant lines of the php-fpm.conf file might look like:
```
[yourapp]
prefix = /var/www
listen = /var/www/fpm/example.sock
user = _exampleuser
group = _exampleuser
chroot = /var/www/example
env[HALFMOON_ENV] = production
php_admin_value[error_log] = /log/production_log
```
Note that your halfmoon application must still live in a directory where Apache can see it if Apache is chrooted, or at least the application's /public directory. This lets Apache directly serve requests for static files.
Overriding the PHP error log (which halfmoon uses to log stats about each request) is recommended to avoid having each line prefixed with even more junk under FastCGI:
```
[error] [client x.x.x.x] FastCGI: server "/public/index.php/" stderr:
```
This also lets the FastCGI daemonized process directly log stats, rather than having to send each line back through the FastCGI socket for Apache to log. Note that the log file referenced will be appended to by the user running the daemonized FastCGI process, so create the /log directory in your halfmoon root and chown it to that user.
Once your halfmoon application's php-fpm process is started and working, the web server configuration will need to be modified to send requests to the FPM socket rather than processing them with its internal PHP module. For Apache, relevant lines might look like this (for a chrooted Apache setup, paths relative to the chroot):
```
AddHandler php-fastcgi .php
Action php-fastcgi /example/fcgi
Alias /example/fcgi /public/index.php
FastCGIExternalServer /public/index.php -socket /fpm/example.sock
RewriteEngine on
RewriteCond %{DOCUMENT_ROOT}/%{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_URI} !^/example/fcgi
RewriteRule ^(.*)$ /index.php [QSA,L]
```
The first 4 lines tell Apache to handle .php files with php-fastcgi, declare an action for those requests and route that action to the /public/index.php file (which interfaces to halfmoon), and send that request over to the FastCGI socket setup by php-fpm.
The previously used mod_rewrite rules are used to send requests for all URLs that don't match local files (such as images, stylesheets, etc.) through halfmoon, with an additional rule added to avoid infinitely looping on requests destined for the FastCGI socket.
There are some differences to be aware of between Rails and halfmoon. Some are due to differences between Ruby and PHP and some are just design changes.
-
The body of the
form_for()
will be executed in a different context, so$this
will not point to the controller as it does elsewhere in the view. To get around this,$C
is defined and (along with any other local variables needed) can be passed into theform_for()
body like so:<h1><?= $C->title() ?></h1> <? $form->form_for($post, "/posts/update", array(), function($f) use ($C) { ?> <h2><?= $C->sub_title(); ?></h2> ... <? }); ?>
This is due to the design of closures in php.
It is recommended to just always use
$C
instead of$this
throughout views and closures. -
list
andnew
are reserved keywords in PHP, so these cannot be used as the controller actions like Rails sets up by default.It is suggested to use
build
instead ofnew
, andindex
instead oflist
. Of course,list
andnew
can still be used in URLs by adding a specific route to map them to different controller actions:HalfMoon\Router::addRoute(array( "url" => ":controller/list", "action" => "index", ));
-
Sessions are disabled by default, but can be enabled per-controller or per-action. In a controller, define a static
$session
variable and either turn it on for the entire controller:static $session = "on";
or just on for specific actions with
except
oronly
arrays:static $session = array( "on" => array( "only" => array("login", "index") ) );
To reverse the settings (enable it for the entire application but disable it for specific actions), define it to "
on
" in yourApplicationController
and then just turn it off per-controller.Note: when using the built-in form helper (form_for) with a
POST
form and XSRF protection is enabled, sessions will be explicitly enabled to allow storing the token in the session pre-POST
and then retrieving it on thePOST
.