a tiny mvc framework for php using php-activerecord
1 .-.
2 ( ( halfmoon
3 `-`
4
5## Overview ##
6
7halfmoon, combined with [php-activerecord](http://github.com/kla/php-activerecord),
8is a tiny MVC framework for PHP 5.3 that tries to use the conventions of
9Ruby on Rails 2.3 wherever possible and reasonable.
10
11It has a similar directory structure to a Rails project, with the root
12level containing models, views, controllers, and helpers directories.
13It supports a concept of environments like Rails, defaulting to a
14development environment which logs things to Apache's error log and
15displays errors in the browser.
16
17Its URL routing works similarly as well, supporting a catch-all default
18route of `:controller/:action/:id` and a root URL (`/`) route.
19
20Form helpers work similar to Rails. For example, doing this in Rails:
21
22```ruby
23<% form_for :post, @post, :url => "/posts/update" do |f| %>
24 <%= f.label :title, "Post Title" %>
25 <%= f.text_field :title, :size => 20 %>
26
27 <%= submit_tag "Submit" %>
28<% end %>
29```
30
31is similar to this in halfmoon:
32
33```HTML+PHP
34<? $form->form_for($post, "/posts/update", array(), function($f) { ?>
35 <?= $f->label("title", "Post Title"); ?>
36 <?= $f->text_field("title", array("size" => 20)); ?>
37
38 <?= $f->submit_button("Submit") ?>
39<? }); ?>
40```
41
42with `$form` being an alias to a FormHelper object automatically setup
43by the controller. There are other helpers available like `$time`,
44`$html`, etc.
45
46`$C` is defined as the current controller object, to access its functions
47such as `render`.
48
49## Requirements ##
50
51- PHP 5.3 or higher with the PDO database extensions you wish to use
52 with php-activerecord (pdo-mysql, pdo-pgsql, etc.).
53
54 The `mcrypt` extension is required for using the encrypted cookie
55 session store (see [this page](http://michaelgracie.com/2009/09/23/plugging-mcrypt-into-php-on-mac-os-x-snow-leopard-10.6.1/) for Mac OS X instructions).
56
57 The `pcntl` extension is required to use `script/dbconsole`. The
58 readline extension is optional, but will improve the use of
59 `script/console`. Both extensions can be installed on Mac OS X with
60 the same instructions for mcrypt but no extra dependencies (download
61 the PHP tarball for the version that `php -v` reports, untar,
62 `cd ext/{pcntl,readline}; phpize; ./configure; make; make install`,
63 enable in php.ini.
64
65- Apache 1 or 2, with mod_rewrite enabled. Development of halfmoon is
66 done on OpenBSD in a chroot()'d Apache 1 server, so any other
67 environment should work fine.
68
69## Installation ##
70
711. (Optional) Create the root directory where you will be storing
72 everything. halfmoon will do this for you but if you are creating
73 it somewhere where you need sudo permissions, do it manually:
74
75 ```
76 $ sudo mkdir /var/www/example/
77 $ sudo chown `whoami` /var/www/example
78 ```
79
802. Fetch the halfmoon source code into your home directory or somewhere
81 convenient (not in the directory you are setting up halfmoon in):
82
83 ```
84 $ git clone git://github.com/jcs/halfmoon.git
85 ```
86
873. Run the halfmoon script to create your skeleton directory at your
88 root directory created in step 1:
89
90 ```
91 $ halfmoon/halfmoon create /var/www/example/
92 copying halfmoon framework... done.
93 creating skeleton directory structure... done.
94 creating random encryption key for session storage... done.
95
96 /var/www/example/:
97 total 14
98 drwxr-xr-x 2 jcs users 512 Feb 15 10:25 config/
99 drwxr-xr-x 2 jcs users 512 Feb 15 10:20 controllers/
100 drwxr-xr-x 5 jcs users 512 Mar 15 20:33 halfmoon/
101 drwxr-xr-x 2 jcs users 512 Mar 15 20:33 helpers/
102 drwxr-xr-x 2 jcs users 512 Mar 15 20:33 models/
103 drwxr-xr-x 4 jcs users 512 Feb 13 19:58 public/
104 drwxr-xr-x 3 jcs users 512 Feb 13 19:58 views/
105
106 welcome to halfmoon!
107 ```
108
109 At a later point, halfmoon will be installed system-wide, so that
110 running "`halfmoon create ...`" will work from anywhere.
111
1124. Setup an Apache Virtual Host with a DocumentRoot pointing to the
113 public/ directory:
114
115 ```ApacheConf
116 <VirtualHost 127.0.0.1>
117 ServerName www.example.com
118
119 CustomLog logs/example_access combined
120
121 # halfmoon will log a few lines for each request (or one
122 # line, or nothing - see config/boot.php) with some
123 # useful information about routing, timing, etc.
124 #
125 # by default these will use php's error_log(), which will
126 # log to the file specified below, but prefixed with
127 # '[error]' and other junk. to log information to a
128 # separate file, create a class that extends and overrides
129 # error(), info(), and warn() methods of \HalfMoon\Log and
130 # use HalfMoon\Config::set_log_handler("YourClass") in your
131 # boot.php file.
132 ErrorLog logs/example_info
133
134 # this should point to your public directory where index.php
135 # lives to interface with halfmoon
136 DocumentRoot /var/www/example/public/
137
138 # try static (cached) pages before dynamic ones
139 DirectoryIndex index.html index.php
140
141 # uncomment in a production environment, otherwise we are
142 # assuming to be running in development
143 #SetEnv HALFMOON_ENV production
144
145 # if suhosin is installed, disable session encryption and
146 # bump the maximum id length since we're handling sessions
147 # on our own
148 php_flag suhosin.session.encrypt off
149 php_value suhosin.session.max_id_length 1024
150
151 # enable mod_rewrite
152 RewriteEngine on
153
154 # handle requests for static assets (stylesheets,
155 # javascript, images, cached pages, etc.) directly
156 RewriteCond %{DOCUMENT_ROOT}/%{REQUEST_FILENAME} !-f
157
158 # route all other requests to halfmoon
159 RewriteRule ^(.*)$ /index.php/%{REQUEST_URI} [QSA,L]
160 </VirtualHost>
161 ```
162
1635. (Optional) Create the database and its tables and grant permissions.
164 Put those settings in the `config/db.ini` file under the development
165 section.
166
167 If you are not using a database, or just don't want to use php-
168 activerecord, remove config/db.ini and php-ar will not be initialized,
169 saving you some minor processing time on each request.
170
171 By default, halfmoon runs in development mode unless the
172 HALFMOON_ENV environment variable is set to something else (such as
173 via the commented out example above, using apache's SetEnv function).
174
175## Usage Overview ##
176
1771. Create models in the `models/` directory according to your database
178 tables.
179
180 Example `models/Post.php`:
181
182 ```php
183 <?php
184
185 class Post extends ActiveRecord\Model {
186 static $belongs_to = array(
187 array("user"),
188 );
189 }
190
191 ?>
192 ```
193
1942. Create controllers in the `controllers/` directory to map urls to
195 actions.
196
197 Example `controllers/posts_controller.rb`:
198
199 ```php
200 <?php
201
202 class PostsController extends ApplicationController {
203 static $before_filter = array("authenticate_user");
204
205 public function index() {
206 $this->posts = Post::find("all");
207 }
208 }
209
210 ?>
211 ```
212
213 To set variables in the namespace of the view, use `$this->varname`.
214 In the above example, `$posts` is an array of all posts and is
215 visible to the view template php file.
216
217 The index action will be called by default when a route does
218 not specify an action.
219
220 Defining a `$before_filter` array of functions will call them before
221 processing the action. If any of them return false (such as one
222 failing to authenticate the user and wanting to redirect to a login
223 page), processing will stop, no other before_filters will be run,
224 and the controller action will not be run.
225
2263. Create views in the `views/` directory. By default, controller
227 actions will try to render `views/*controller*/*action*.phtml`.
228 For example, these URLs:
229
230 ```
231 /posts
232 /posts/index
233 ```
234
235 will both call the `index` action in `PostsController`, which will
236 render `views/posts/index.phtml`.
237
238 A URL of:
239
240 ```
241 /posts/show/1
242 ```
243
244 would map (using the default catch-all route) to the posts
245 controller, calling the `show` action with `$id` set to 1, and then
246 render `views/posts/show.phtml`.
247
248 Partial views are snippets of HTML that are shared among views and
249 can be included in a view template with render function. Their
250 filenames must start with underscores.
251
252 For example, if `views/posts/index.phtml` contained:
253
254 ```php
255 <?php
256
257 $C->render(array("partial" => "header_image"));
258 ...
259
260 ?>
261 ```
262
263 then `views/posts/_header_image.phtml` would be brought in.
264
265 After a controller renders its view file, it is stored in the
266 `$content_for_layout` variable and the `views/layouts/application.phtml`
267 file is rendered. Be sure to print `$content_for_layout` somewhere in
268 that file.
269
2704. (Optional) Configure a root route to specify which controller/action
271 should be used for viewing the root (`/`) URL via `config/routes.php`:
272
273 ```php
274 HalfMoon\Router::addRootRoute(array(
275 "controller" => "posts",
276 "action" => "homepage"
277 ));
278 ```
279
280 this uses the same rules as other routes, calling the `index` action
281 if it is not specified.
282
283 If your site should always present a static page (like a
284 login/splash page) at the root URL, then simply make a
285 public/index.html file to avoid processing through halfmoon. This
286 is handled entirely outside of halfmoon by apache, because of the
287 `mod_rewrite` rule.
288
2895. Change or create site-specific and environment-specific settings in
290 the `config/boot.php` script. This can be used to adjust logging,
291 tweak PHP settings, or set global PHP variables that you need.
292
293## Moving to Production ##
294
2951. Copy the entire directory tree (/var/www/example in this example)
296 somewhere, setup an Apache Virtual Host like the example above, but
297 use the `SetEnv` apache function to change the `HALFMOON_ENV`
298 environment to "production".
299
300 ```ApacheConf
301 <VirtualHost ...>
302 ...
303 SetEnv HALFMOON_ENV production
304 ...
305 </VirtualHost>
306 ```
307
308 This will use the database configured in `config/db.ini` under the
309 production group, and any settings you have changed in
310 `config/boot.php` that are environment-specific (such as disabling
311 logging).
312
3132. Verify that your static 404 and 500 pages (in `public/`) have useful
314 content.
315
316 You may wish to turn halfmoon's logging off completely, instead of
317 the "short" style used by default in production which will only log
318 one line logging the processing time for each request. This can be
319 adjusted in `config/boot.php`:
320
321 ```php
322 HalfMoon\Config::set_activerecord_log_level("none");
323 ```
324
325 It is also recommended that you enable exception notification
326 e-mails, which will e-mail you a backtrace and some helpful
327 debugging information any time an error happens in your application:
328
329 ```php
330 HalfMoon\Config::set_exception_notification_recipient("you@example.com");
331 HalfMoon\Config::set_exception_notification_subject("[your app]");
332 ```
333
334## Using halfmoon with FastCGI ##
335
336Newer versions of PHP include optional [FPM](http://php.net/manual/en/install.fpm.php) support which makes
337it easy to run halfmoon applications in a secure environment and, when
338combined with [APC](http://php.net/manual/en/book.apc.php), can increase performance by caching PHP code
339between requests. The halfmoon framework and models will not need to be
340reloaded and re-parsed on every request.
341
342After installing PHP with FPM support and the APC PECL module, FPM can
343be configured to chroot to your halfmoon application's root directory,
344and run the application as a specific user. To extend the configuration
345of the example Apache configuration above, the relevant lines of the
346php-fpm.conf file might look like:
347
348 ```
349 [yourapp]
350 prefix = /var/www
351 listen = /var/www/fpm/example.sock
352 user = _exampleuser
353 group = _exampleuser
354 chroot = /var/www/example
355 env[HALFMOON_ENV] = production
356 php_admin_value[error_log] = /log/production_log
357 ```
358
359Note that your halfmoon application must still live in a directory where
360Apache can see it if Apache is chrooted, or at least the application's
361/public directory. This lets Apache directly serve requests for static
362files.
363
364Overriding the PHP error log (which halfmoon uses to log stats about
365each request) is recommended to avoid having each line prefixed with
366even more junk under FastCGI:
367
368 ```
369 [error] [client x.x.x.x] FastCGI: server "/public/index.php/" stderr:
370 ```
371
372This also lets the FastCGI daemonized process directly log stats, rather
373than having to send each line back through the FastCGI socket for Apache
374to log. Note that the log file referenced will be appended to by the
375user running the daemonized FastCGI process, so create the /log
376directory in your halfmoon root and chown it to that user.
377
378Once your halfmoon application's php-fpm process is started and working,
379the web server configuration will need to be modified to send requests
380to the FPM socket rather than processing them with its internal PHP
381module. For Apache, relevant lines might look like this (for a chrooted
382Apache setup, paths relative to the chroot):
383
384 ```
385 AddHandler php-fastcgi .php
386 Action php-fastcgi /example/fcgi
387 Alias /example/fcgi /public/index.php
388 FastCGIExternalServer /public/index.php -socket /fpm/example.sock
389
390 RewriteEngine on
391 RewriteCond %{DOCUMENT_ROOT}/%{REQUEST_FILENAME} !-f
392 RewriteCond %{REQUEST_URI} !^/example/fcgi
393 RewriteRule ^(.*)$ /index.php [QSA,L]
394 ```
395
396The first 4 lines tell Apache to handle .php files with php-fastcgi,
397declare an action for those requests and route that action to the
398/public/index.php file (which interfaces to halfmoon), and send that
399request over to the FastCGI socket setup by php-fpm.
400
401The previously used mod_rewrite rules are used to send requests for all
402URLs that don't match local files (such as images, stylesheets, etc.)
403through halfmoon, with an additional rule added to avoid infinitely
404looping on requests destined for the FastCGI socket.
405
406## Caveats ##
407
408There are some differences to be aware of between Rails and halfmoon.
409Some are due to differences between Ruby and PHP and some are just
410design changes.
411
4121. The body of the `form_for()` will be executed in a different context,
413 so `$this` will not point to the controller as it does elsewhere in
414 the view. To get around this, `$C` is defined and (along with any
415 other local variables needed) can be passed into the `form_for()`
416 body like so:
417
418 ```HTML+PHP
419 <h1><?= $C->title() ?></h1>
420
421 <? $form->form_for($post, "/posts/update", array(), function($f) use ($C) { ?>
422 <h2><?= $C->sub_title(); ?></h2>
423 ...
424 <? }); ?>
425 ```
426
427 This is due to the [design of closures in php](http://wiki.php.net/rfc/closures/removal-of-this).
428
429 It is recommended to just always use `$C` instead of `$this`
430 throughout views and closures.
431
4322. `list` and `new` are reserved keywords in PHP, so these cannot be
433 used as the controller actions like Rails sets up by default.
434
435 It is suggested to use `build` instead of `new`, and `index` instead
436 of `list`. Of course, `list` and `new` can still be used in URLs by
437 adding a specific route to map them to different controller actions:
438
439 ```php
440 HalfMoon\Router::addRoute(array(
441 "url" => ":controller/list",
442 "action" => "index",
443 ));
444 ```
445
4463. Sessions are disabled by default, but can be enabled per-controller
447 or per-action. In a controller, define a static `$session` variable
448 and either turn it on for the entire controller:
449
450 ```php
451 static $session = "on";
452 ```
453
454 or just on for specific actions with `except` or `only` arrays:
455
456 ```php
457 static $session = array(
458 "on" => array(
459 "only" => array("login", "index")
460 )
461 );
462 ```
463
464 To reverse the settings (enable it for the entire application but
465 disable it for specific actions), define it to "`on`" in your
466 `ApplicationController` and then just turn it off per-controller.
467
468 Note: when using the built-in form helper (form_for) with a `POST`
469 form and XSRF protection is enabled, sessions will be explicitly
470 enabled to allow storing the token in the session pre-`POST` and then
471 retrieving it on the `POST`.