a tiny mvc framework for php using php-activerecord
at v1 471 lines 16 kB view raw view rendered
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`.